<commit_before>#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Staples is a simple static site generator based on the idea of processors
mapped to types of files. It includes a basic server for development, and
has no external requirements itself, beyond the Python Standard Library.
Specific processors, such as the included Django template renderer, will
have their own requirements.

Info and source: http://github.com/typeish/Staples

Alec Perkins, type(ish) inc
License: UNLICENSE
"""

__author__ = 'type(ish) inc'
__version__ = '0.1a'
__license__ = 'UNLICENSE'

import os, shutil, commands, glob, sys

# Default settings
PROJECT_ROOT = os.getcwd()
CONTENT_DIR = os.path.join(PROJECT_ROOT, 'content')
DEPLOY_DIR = os.path.join(PROJECT_ROOT, 'deploy')

REMOTE_ROOT = ''
DEPLOY_TRACKING_FILE = os.path.join(PROJECT_ROOT, 'staples_deploy_info.p')

INDEX_FILE = 'index.html'
IGNORE = ()
PROCESSORS = {}

# Look for a settings.py in the current working directory
sys.path.append(PROJECT_ROOT)
try:
    import settings
    from settings import *
except ImportError:
    if __name__ == "__main__":
        print 'No settings.py found, using defaults.\n'
else:
    if __name__ == "__main__":
        print 'Using settings file %s' % settings.__file__[:-1]

verbose=False

class File(object):

    def __init__(self, file_path, parent_ignored=False, **kwargs):
        self.source = file_path
        self.rel_path = file_path.replace(CONTENT_DIR,'').lstrip('/')
        self.rel_parent, self.name = os.path.split(self.source)
        self.ext = os.path.splitext(self.name)[1]
        self.parent_ignored = parent_ignored

    def process(self, **kwargs):
        if verbose:
            print 'Processing:', self.rel_path
        if self.is_directory:
            if '/' in PROCESSORS:
                p = PROCESSORS['/']
            else:
                p = handle_directory
        elif self.name in PROCESSORS:
            p = PROCESSORS[self.name]
        elif self.ext in PROCESSORS:
            p = PROCESSORS[self.ext]
        elif '*' in PROCESSORS:
            p = PROCESSORS['*']
        else:
            p = handle_others
        return p(self, **kwargs)

    @property
    def deploy_path(self): return os.path.join(DEPLOY_DIR, self.rel_path)

    @property
    def remote_path(self): return os.path.join(REMOTE_ROOT, self.rel_path)

    @property
    def mtime(self): return os.path.getmtime(self.source)

    @property
    def is_directory(self): return os.path.isdir(self.source)

    @property
    def ignorable(self):
        return ( self.name.startswith(".") or self.name.startswith("_")
                or self.name.endswith("~") or self.name in IGNORE )

# BUILD FUNCTIONS
###############################################################################
def build(**kwargs):
    print 'Starting build...\n'
    if verbose:
        print 'Removing any existing deploy directory'
    shutil.rmtree(DEPLOY_DIR, ignore_errors=True)

    if verbose:
        print 'Creating deploy directory: ', DEPLOY_DIR
    os.mkdir(DEPLOY_DIR)

    if verbose:
        print 'Traversing content directory: %s...' % CONTENT_DIR

    build_directories(CONTENT_DIR, **kwargs)

    print '\nBuild done'

def build_directories(t_path, **kwargs):
    """
    Recursively traverses a given directory, calling the given file's handler.
    Keyword arguments are passed through to the handler.
    """
    for file in glob.glob( os.path.join(t_path, '*') ):
        File(file, **kwargs).process(**kwargs)

# DEFAULT HANDLERS
# These two functions basically just copy anything they are given over to
# the deploy directory.

def handle_directory(f, **kwargs):
    """
    Copies directories, unless they are ignorable or their
    parent was ignored.
    """
    if not f.ignorable and not f.parent_ignored:
        try:
            os.mkdir(f.deploy_path)
        except OSError:
            pass
    else:
        kwargs.update({'parent_ignored': True})
    build_directories(f.source, **kwargs)

def handle_others(f, **kwargs):
    """
    Simply copies files from the source path to the deploy path.
    """
    if not f.ignorable and not f.parent_ignored:
        if verbose:
            print 'Copying file to:', f.deploy_path
        commands.getoutput(u"cp %s %s" % (f.source, f.deploy_path))
    elif verbose:
        print 'Ignoring:', f.rel_path

# EXTRA HANDLERS

def handle_django(f, for_deployment=False, **kwargs):
    """
    Renders templates using the Django template rendering engine. If the
    template ends in .django, the resulting output filename has that removed.

    Requires:
        Django - any version that can handle the templates you give it
        settings.py - placed in the same directory and defines both
                      TEMPLATE_DIRS and CONTEXT
    """
    verbose = globals().get('verbose', False)
    if not f.ignorable and not f.parent_ignored:
        from django.template.loader import render_to_string
        import settings
        os.environ['DJANGO_SETTINGS_MODULE'] = u"settings"
        deploy_path = f.deploy_path.replace('.django','')

        if verbose:
            print 'Rendering:', f.rel_path

        context = {}
        if settings.CONTEXT:
            context = settings.CONTEXT
        context['for_deployment'] = for_deployment
        rendered = render_to_string(f.source, context)

        if verbose:
            print 'Saving rendered output to:', deploy_path
        fout = open(deploy_path,'w')
        fout.write(rendered)
        fout.close()
    elif verbose:
        print 'Ignoring:', f.rel_path

def handle_gallery(f, **kwargs):
    pass
    # build a gallery using a folder of images, creating an index.html file
    # with all of the images, and an N.html file for each image with the full
    # sized image, up to CONTEXT.max_width

def handle_markdown(f, **kwargs):
    """
    A markdown processor. Requires the `markdown` module. It allows for
    prepend and append code to wrap the rendered page in proper HTML structure.
    Includes support for a meta-information notation in the markdown source
    files for title and meta tags.

    The extension of the source file is replaced with `.html`.
    """
    if not f.ignorable and not f.parent_ignored:
        from markdown import markdown
        import codecs, re
        json = None

        try:
            import simplejson as json
        except ImportError:
            try:
                import json
            except ImportError:
                pass

        try:
            from settings import MD_PRE
        except ImportError:
            pre = ''
        else:
            pre = MD_PRE

        try:
            from settings import MD_POST
        except ImportError:
            post = ''
        else:
            post = MD_POST

        info = {}
        if json:
            text = codecs.open(f.source, mode="r", encoding="utf8").read()

            pattern = re.compile('!INFO.+\/INFO',re.DOTALL)
            result = re.match(pattern,text)
            if result:
                info = result.group()
                text = text.replace(info,'')
                info = info.lstrip('!INFO').rstrip('/INFO')
                info = json.loads(info)

        print 'Rendering:',f.rel_path, 'with INFO' if info else ''
        rendered = markdown(text)

        deploy_path = f.deploy_path.replace(f.ext, '.html')
        fout = open(deploy_path, 'w')
        if len(info) > 0:
            for k in info:
                if k[:4] == 'meta':
                    tag = '<meta type="%s" content="' % k.split('-')[1]
                else:
                    tag = '<%s>' % k
                pre = pre.replace(tag, tag + info[k])

        fout.write(pre + rendered + post)
        fout.close()

# WATCH FUNCTIONS
###############################################################################

import datetime, time

def watch():
    """
    Initiates a full rebuild of the project, then watches for changed files.
    Once a second, it polls all of the files in the content directory.
    """
    watcher = DirWatcher(CONTENT_DIR)
    build()
    print 'Watching %s...' % CONTENT_DIR
    while True:
        time.sleep(1)
        changed = watcher.find_changed_files()
        if len(changed) > 0:
            for f in changed:
                f.process()

class DirWatcher(object):
    """
    Class that keeps track of the files in the content directory, and their
    modification times, so they can be watched for changes.
    """
    def __init__(self, directory, update=True):
        self.target_directory = directory
        self.changed_files = []
        self.file_list = {}
        if update:
            self.update_mtimes(self.target_directory)

    def find_changed_files(self):
        self.changed_files = []
        self.update_mtimes(self.target_directory)
        return self.changed_files

    def update_mtimes(self, current_file):
        """
        Recursively traverses the directory, updating the dictionary of mtimes
        and the list of files changed since the last check.
        """
        for file in glob.glob( os.path.join(current_file, '*') ):
            f = File(file)
            if f.is_directory:
                f.process()
                self.update_mtimes(f.source)
            else:
                mtime = f.mtime
                if not f.source in self.file_list or self.file_list[f.source] != mtime:
                    self.file_list[f.source] = mtime
                    self.changed_files.append(f)

# DEVELOPMENT SERVER
###############################################################################

from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import mimetypes

class HandleRequests(BaseHTTPRequestHandler):
    """
    A stupid-simple webserver that serves up static files, and nothing else.
    Requests for a directory will return the contents of the INDEX_FILE.
    """
    def do_GET(self):
        try:
            path_append = ''
            if len(self.path) > 0 and self.path[-1] == '/':
                self.path = self.path + INDEX_FILE
            file_path = DEPLOY_DIR + self.path + path_append
            mtype = mimetypes.guess_type(file_path)[0]
            f = open(file_path)
            self.send_response(200)
            self.send_header('Content-type', mtype)
            self.end_headers()
            self.wfile.write(f.read())
            f.close()
            return

        except IOError:
            self.send_error(404,'File Not Found: %s' % self.path)

def runserver(port=8000):
    """
    Runs the web server at localhost and the specified port (default port 8000).
    """
    try:
        server = HTTPServer(('', port), HandleRequests)
        print 'Running server at localhost:%s...' % port
        server.serve_forever()
    except KeyboardInterrupt:
        print 'Shutting down server'
        server.socket.close()

# CONTROL
###############################################################################

# START Autoreloading launcher.
# Borrowed from Peter Hunt and the CherryPy project (http://www.cherrypy.org).
# Some taken from Ian Bicking's Paste (http://pythonpaste.org/).
#
# Portions copyright (c) 2004, CherryPy Team (team@cherrypy.org)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#     * Redistributions of source code must retain the above copyright notice,
#       this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright notice,
#       this list of conditions and the following disclaimer in the documentation
#       and/or other materials provided with the distribution.
#     * Neither the name of the CherryPy Team nor the names of its contributors
#       may be used to endorse or promote products derived from this software
#       without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import os, sys, time

try:
    import thread
except ImportError:
    import dummy_thread as thread

RUN_RELOADER = True

_mtimes = {}
_win = (sys.platform == "win32")

def code_changed():
    global _mtimes, _win
    for filename in filter(lambda v: v, map(lambda m: getattr(m, "__file__", None), sys.modules.values())):
        if filename.endswith(".pyc") or filename.endswith(".pyo"):
            filename = filename[:-1]
        if not os.path.exists(filename):
            continue # File might be in an egg, so it can't be reloaded.
        stat = os.stat(filename)
        mtime = stat.st_mtime
        if _win:
            mtime -= stat.st_ctime
        if filename not in _mtimes:
            _mtimes[filename] = mtime
            continue
        if mtime != _mtimes[filename]:
            _mtimes = {}
            return True
    return False

def reloader_thread():
    while RUN_RELOADER:
        if code_changed():
            sys.exit(3) # force reload
        time.sleep(1)

def restart_with_reloader():
    while True:
        args = [sys.executable] + sys.argv
        if sys.platform == "win32":
            args = ['"%s"' % arg for arg in args]
        new_environ = os.environ.copy()
        new_environ["RUN_MAIN"] = 'true'
        exit_code = os.spawnve(os.P_WAIT, sys.executable, args, new_environ)
        if exit_code != 3:
            return exit_code

def python_reloader(main_func, args, kwargs):
    if os.environ.get("RUN_MAIN") == "true":
        thread.start_new_thread(main_func, args, kwargs)
        try:
            reloader_thread()
        except KeyboardInterrupt:
            pass
    else:
        try:
            sys.exit(restart_with_reloader())
        except KeyboardInterrupt:
            pass

def jython_reloader(main_func, args, kwargs):
    from _systemrestart import SystemRestart
    thread.start_new_thread(main_func, args)
    while True:
        if code_changed():
            raise SystemRestart
        time.sleep(1)

def autoreload_main(main_func, args=None, kwargs=None):
    if args is None:
        args = ()
    if kwargs is None:
        kwargs = {}
    if sys.platform.startswith('java'):
        reloader = jython_reloader
    else:
        reloader = python_reloader
    reloader(main_func, args, kwargs)
# END Autoreload launcher.

if __name__ == "__main__":
    if '-v' in sys.argv:
        verbose = True
    else:
        verbose = False

    if 'runserver' in sys.argv:
        port = 8000
        try:
            port = int(sys.argv[2])
        except:
            pass
        thread.start_new_thread(watch, ())
        autoreload_main(runserver, (port,))

    elif 'build' in sys.argv:
        if '-d' in sys.argv:
            build(for_deployment=True)
        else:
            build()

    elif 'watch' in sys.argv:
        watch()
    else:
        print """
    Staples Usage:
        build     - `python staples.py build`
        watch     - `python staples.py watch`
        runserver - `python staples.py runserver [port]`'

    Add '-v' to any command for verbose output.
    e.g. `python staples.py build verbose`

    Add '-d' to `build` for building with for_deployment set to True.
    e.g. `python staples.py build -d`
    """
<commit_msg>Added strip_extension helper function and did some quote and whitespace cleanup.<commit_after>#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Staples is a simple static site generator based on the idea of processors
mapped to types of files. It includes a basic server for development, and
has no external requirements itself, beyond the Python Standard Library.
Specific processors, such as the included Django template renderer, will
have their own requirements.

Info and source: http://github.com/typeish/Staples

Alec Perkins, type(ish) inc
License: UNLICENSE
"""

__author__ = 'type(ish) inc'
__version__ = '0.1a'
__license__ = 'UNLICENSE'

import os, shutil, commands, glob, sys

# Default settings
PROJECT_ROOT = os.getcwd()
CONTENT_DIR = os.path.join(PROJECT_ROOT, 'content')
DEPLOY_DIR = os.path.join(PROJECT_ROOT, 'deploy')

REMOTE_ROOT = ''
DEPLOY_TRACKING_FILE = os.path.join(PROJECT_ROOT, 'staples_deploy_info.p')

INDEX_FILE = 'index.html'
IGNORE = ()
PROCESSORS = {}

# Look for a settings.py in the current working directory
sys.path.append(PROJECT_ROOT)
try:
    import settings
    from settings import *
except ImportError:
    if __name__ == "__main__":
        print 'No settings.py found, using defaults.\n'
else:
    if __name__ == "__main__":
        print 'Using settings file %s' % settings.__file__[:-1]

verbose=False

class File(object):

    def __init__(self, file_path, parent_ignored=False, **kwargs):
        self.source = file_path
        self.rel_path = file_path.replace(CONTENT_DIR,'').lstrip('/')
        self.rel_parent, self.name = os.path.split(self.source)
        self.ext = os.path.splitext(self.name)[1]
        self.parent_ignored = parent_ignored

    def process(self, **kwargs):
        if verbose:
            print 'Processing:', self.rel_path
        if self.is_directory:
            if '/' in PROCESSORS:
                p = PROCESSORS['/']
            else:
                p = handle_directory
        elif self.name in PROCESSORS:
            p = PROCESSORS[self.name]
        elif self.ext in PROCESSORS:
            p = PROCESSORS[self.ext]
        elif '*' in PROCESSORS:
            p = PROCESSORS['*']
        else:
            p = handle_others
        return p(self, **kwargs)

    @property
    def deploy_path(self): return os.path.join(DEPLOY_DIR, self.rel_path)

    @property
    def remote_path(self): return os.path.join(REMOTE_ROOT, self.rel_path)

    @property
    def mtime(self): return os.path.getmtime(self.source)

    @property
    def is_directory(self): return os.path.isdir(self.source)

    @property
    def ignorable(self):
        return ( self.name.startswith(".") or self.name.startswith("_")
                or self.name.endswith("~") or self.name in IGNORE )

# BUILD FUNCTIONS
###############################################################################
def build(**kwargs):
    print 'Starting build...\n'
    if verbose:
        print 'Removing any existing deploy directory'
    shutil.rmtree(DEPLOY_DIR, ignore_errors=True)

    if verbose:
        print 'Creating deploy directory: ', DEPLOY_DIR
    os.mkdir(DEPLOY_DIR)

    if verbose:
        print 'Traversing content directory: %s...' % CONTENT_DIR

    build_directories(CONTENT_DIR, **kwargs)

    print '\nBuild done'

def build_directories(t_path, **kwargs):
    """
    Recursively traverses a given directory, calling the given file's handler.
    Keyword arguments are passed through to the handler.
    """
    for file in glob.glob( os.path.join(t_path, '*') ):
        File(file, **kwargs).process(**kwargs)

# HELPER FUNCTIONS

def strip_extension(filepath, ext):
    """
    Safely strips file extensions.
    """
    if not ext.startswith("."):
        ext = "." + ext
    dirname = os.path.dirname(filepath)
    basename = os.path.basename(filepath).replace(ext, "")
    if not basename:
        raise ValueError("Stripping '%s' will cause the file name to be blank." % ext)
    return os.path.join(dirname, basename)

# DEFAULT HANDLERS
# These two functions basically just copy anything they are given over to
# the deploy directory.

def handle_directory(f, **kwargs):
    """
    Copies directories, unless they are ignorable or their
    parent was ignored.
    """
    if not f.ignorable and not f.parent_ignored:
        try:
            os.mkdir(f.deploy_path)
        except OSError:
            pass
    else:
        kwargs.update({'parent_ignored': True})
    build_directories(f.source, **kwargs)

def handle_others(f, **kwargs):
    """
    Simply copies files from the source path to the deploy path.
    """
    if not f.ignorable and not f.parent_ignored:
        if verbose:
            print 'Copying file to:', f.deploy_path
        commands.getoutput(u"cp %s %s" % (f.source, f.deploy_path))
    elif verbose:
        print 'Ignoring:', f.rel_path

# EXTRA HANDLERS

def handle_django(f, for_deployment=False, **kwargs):
    """
    Renders templates using the Django template rendering engine. If the
    template ends in .django, the resulting output filename has that removed.

    Requires:
        Django - any version that can handle the templates you give it
        settings.py - placed in the same directory and defines both
                      TEMPLATE_DIRS and CONTEXT
    """
    verbose = globals().get('verbose', False)
    if not f.ignorable and not f.parent_ignored:
        from django.template.loader import render_to_string
        import settings

        os.environ['DJANGO_SETTINGS_MODULE'] = u"settings"
        deploy_path = strip_extension(f.deploy_path, "django")

        if verbose:
            print "Rendering:", f.rel_path

        context = {}
        if settings.CONTEXT:
            context = settings.CONTEXT
        context["for_deployment"] = for_deployment
        rendered = render_to_string(f.source, context)

        if verbose:
            print "Saving rendered output to:", deploy_path
        fout = open(deploy_path,"w")
        fout.write(rendered)
        fout.close()
    elif verbose:
        print "Ignoring:", f.rel_path

def handle_gallery(f, **kwargs):
    pass
    # build a gallery using a folder of images, creating an index.html file
    # with all of the images, and an N.html file for each image with the full
    # sized image, up to CONTEXT.max_width

def handle_markdown(f, **kwargs):
    """
    A markdown processor. Requires the `markdown` module. It allows for
    prepend and append code to wrap the rendered page in proper HTML structure.
    Includes support for a meta-information notation in the markdown source
    files for title and meta tags.

    The extension of the source file is replaced with `.html`.
    """
    if not f.ignorable and not f.parent_ignored:
        from markdown import markdown
        import codecs, re
        json = None

        try:
            import simplejson as json
        except ImportError:
            try:
                import json
            except ImportError:
                pass

        try:
            from settings import MD_PRE
        except ImportError:
            pre = ''
        else:
            pre = MD_PRE

        try:
            from settings import MD_POST
        except ImportError:
            post = ''
        else:
            post = MD_POST

        info = {}
        if json:
            text = codecs.open(f.source, mode="r", encoding="utf8").read()

            pattern = re.compile('!INFO.+\/INFO',re.DOTALL)
            result = re.match(pattern,text)
            if result:
                info = result.group()
                text = text.replace(info,'')
                info = info.lstrip('!INFO').rstrip('/INFO')
                info = json.loads(info)

        print 'Rendering:',f.rel_path, 'with INFO' if info else ''
        rendered = markdown(text)

        deploy_path = f.deploy_path.replace(f.ext, '.html')
        fout = open(deploy_path, 'w')
        if len(info) > 0:
            for k in info:
                if k[:4] == 'meta':
                    tag = '<meta type="%s" content="' % k.split('-')[1]
                else:
                    tag = '<%s>' % k
                pre = pre.replace(tag, tag + info[k])

        fout.write(pre + rendered + post)
        fout.close()

# WATCH FUNCTIONS
###############################################################################

import datetime, time

def watch():
    """
    Initiates a full rebuild of the project, then watches for changed files.
    Once a second, it polls all of the files in the content directory.
    """
    watcher = DirWatcher(CONTENT_DIR)
    build()
    print 'Watching %s...' % CONTENT_DIR
    while True:
        time.sleep(1)
        changed = watcher.find_changed_files()
        if len(changed) > 0:
            for f in changed:
                f.process()

class DirWatcher(object):
    """
    Class that keeps track of the files in the content directory, and their
    modification times, so they can be watched for changes.
    """
    def __init__(self, directory, update=True):
        self.target_directory = directory
        self.changed_files = []
        self.file_list = {}
        if update:
            self.update_mtimes(self.target_directory)

    def find_changed_files(self):
        self.changed_files = []
        self.update_mtimes(self.target_directory)
        return self.changed_files

    def update_mtimes(self, current_file):
        """
        Recursively traverses the directory, updating the dictionary of mtimes
        and the list of files changed since the last check.
        """
        for file in glob.glob( os.path.join(current_file, '*') ):
            f = File(file)
            if f.is_directory:
                f.process()
                self.update_mtimes(f.source)
            else:
                mtime = f.mtime
                if not f.source in self.file_list or self.file_list[f.source] != mtime:
                    self.file_list[f.source] = mtime
                    self.changed_files.append(f)

# DEVELOPMENT SERVER
###############################################################################

from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import mimetypes

class HandleRequests(BaseHTTPRequestHandler):
    """
    A stupid-simple webserver that serves up static files, and nothing else.
    Requests for a directory will return the contents of the INDEX_FILE.
    """
    def do_GET(self):
        try:
            path_append = ''
            if len(self.path) > 0 and self.path[-1] == '/':
                self.path = self.path + INDEX_FILE
            file_path = DEPLOY_DIR + self.path + path_append
            mtype = mimetypes.guess_type(file_path)[0]
            f = open(file_path)
            self.send_response(200)
            self.send_header('Content-type', mtype)
            self.end_headers()
            self.wfile.write(f.read())
            f.close()
            return

        except IOError:
            self.send_error(404,'File Not Found: %s' % self.path)

def runserver(port=8000):
    """
    Runs the web server at localhost and the specified port (default port 8000).
    """
    try:
        server = HTTPServer(('', port), HandleRequests)
        print 'Running server at localhost:%s...' % port
        server.serve_forever()
    except KeyboardInterrupt:
        print 'Shutting down server'
        server.socket.close()

# CONTROL
###############################################################################

# START Autoreloading launcher.
# Borrowed from Peter Hunt and the CherryPy project (http://www.cherrypy.org).
# Some taken from Ian Bicking's Paste (http://pythonpaste.org/).
#
# Portions copyright (c) 2004, CherryPy Team (team@cherrypy.org)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#     * Redistributions of source code must retain the above copyright notice,
#       this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright notice,
#       this list of conditions and the following disclaimer in the documentation
#       and/or other materials provided with the distribution.
#     * Neither the name of the CherryPy Team nor the names of its contributors
#       may be used to endorse or promote products derived from this software
#       without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import os, sys, time

try:
    import thread
except ImportError:
    import dummy_thread as thread

RUN_RELOADER = True

_mtimes = {}
_win = (sys.platform == "win32")

def code_changed():
    global _mtimes, _win
    for filename in filter(lambda v: v, map(lambda m: getattr(m, "__file__", None), sys.modules.values())):
        if filename.endswith(".pyc") or filename.endswith(".pyo"):
            filename = filename[:-1]
        if not os.path.exists(filename):
            continue # File might be in an egg, so it can't be reloaded.
        stat = os.stat(filename)
        mtime = stat.st_mtime
        if _win:
            mtime -= stat.st_ctime
        if filename not in _mtimes:
            _mtimes[filename] = mtime
            continue
        if mtime != _mtimes[filename]:
            _mtimes = {}
            return True
    return False

def reloader_thread():
    while RUN_RELOADER:
        if code_changed():
            sys.exit(3) # force reload
        time.sleep(1)

def restart_with_reloader():
    while True:
        args = [sys.executable] + sys.argv
        if sys.platform == "win32":
            args = ['"%s"' % arg for arg in args]
        new_environ = os.environ.copy()
        new_environ["RUN_MAIN"] = 'true'
        exit_code = os.spawnve(os.P_WAIT, sys.executable, args, new_environ)
        if exit_code != 3:
            return exit_code

def python_reloader(main_func, args, kwargs):
    if os.environ.get("RUN_MAIN") == "true":
        thread.start_new_thread(main_func, args, kwargs)
        try:
            reloader_thread()
        except KeyboardInterrupt:
            pass
    else:
        try:
            sys.exit(restart_with_reloader())
        except KeyboardInterrupt:
            pass

def jython_reloader(main_func, args, kwargs):
    from _systemrestart import SystemRestart
    thread.start_new_thread(main_func, args)
    while True:
        if code_changed():
            raise SystemRestart
        time.sleep(1)

def autoreload_main(main_func, args=None, kwargs=None):
    if args is None:
        args = ()
    if kwargs is None:
        kwargs = {}
    if sys.platform.startswith('java'):
        reloader = jython_reloader
    else:
        reloader = python_reloader
    reloader(main_func, args, kwargs)
# END Autoreload launcher.

if __name__ == "__main__":
    if '-v' in sys.argv:
        verbose = True
    else:
        verbose = False

    if 'runserver' in sys.argv:
        port = 8000
        try:
            port = int(sys.argv[2])
        except:
            pass
        thread.start_new_thread(watch, ())
        autoreload_main(runserver, (port,))

    elif 'build' in sys.argv:
        if '-d' in sys.argv:
            build(for_deployment=True)
        else:
            build()

    elif 'watch' in sys.argv:
        watch()
    else:
        print """
    Staples Usage:
        build     - `python staples.py build`
        watch     - `python staples.py watch`
        runserver - `python staples.py runserver [port]`'

    Add '-v' to any command for verbose output.
    e.g. `python staples.py build verbose`

    Add '-d' to `build` for building with for_deployment set to True.
    e.g. `python staples.py build -d`
    """
<|endoftext|>


<commit_before>"""Simple single-process sequential image processing."""

import datetime
import sys
import cv2
import numpy as np

import util
import iproc

DEVICE   = int(sys.argv[1])
WIDTH    = int(sys.argv[2])
HEIGHT   = int(sys.argv[3])
DURATION = float(sys.argv[4])  # In seconds.

# Create the OpenCV video capture object.
cap = cv2.VideoCapture(DEVICE)
cap.set(3, WIDTH)
cap.set(4, HEIGHT)

# Create the output window.
cv2.namedWindow('motion 1', cv2.cv.CV_WINDOW_NORMAL)

# Maintain accumulation of thresholded differences.
image_acc = None

# Keep track of previous iteration's timestamp.
tstamp_prev = None

# Monitor framerates for the given seconds past.
framerate = util.RateTicker((1,5,10))

# Run the loop for designated amount of time.
end = datetime.datetime.now() + datetime.timedelta(seconds=DURATION)
while end > datetime.datetime.now():

    # Take a snapshot and mark the snapshot time.
    hello, image = cap.read()

    # Compute alpha value.
    alpha, tstamp_prev = iproc.getAlpha(tstamp_prev)

    # Preprocess the image.
    image_pre = iproc.preprocess2(image)

    # Initalize accumulation if so indicated.
    if image_acc is None:
        image_acc = np.empty(np.shape(image_pre))

    # Compute difference.
    image_diff = cv2.absdiff(
        image_acc.astype(image_pre.dtype),
        image_pre,
        )

    # Accumulate.
    hello = cv2.accumulateWeighted(
        image_pre,
        image_acc,
        alpha,
        )

    # Draw the difference on top of the image.
    iproc.postprocess(image, image_diff)

    # Write the framerate on top of the image.
    iproc.writeOSD(
        image,
        ('%.2f, %.2f, %.2f fps'%framerate.tick(),),
        ratio=0.04,
        )

    # Display the image.
    cv2.imshow('motion 1', image)

    # Allow HighGUI to process event.
    cv2.waitKey(1)

# The end.
<commit_msg>Add to top docstring.<commit_after>"""Simple single-process sequential image processing
doing motion detection."""

import datetime
import sys
import cv2
import numpy as np

import util
import iproc

DEVICE   = int(sys.argv[1])
WIDTH    = int(sys.argv[2])
HEIGHT   = int(sys.argv[3])
DURATION = float(sys.argv[4])  # In seconds.

# Create the OpenCV video capture object.
cap = cv2.VideoCapture(DEVICE)
cap.set(3, WIDTH)
cap.set(4, HEIGHT)

# Create the output window.
cv2.namedWindow('motion 1', cv2.cv.CV_WINDOW_NORMAL)

# Maintain accumulation of thresholded differences.
image_acc = None

# Keep track of previous iteration's timestamp.
tstamp_prev = None

# Monitor framerates for the given seconds past.
framerate = util.RateTicker((1,5,10))

# Run the loop for designated amount of time.
end = datetime.datetime.now() + datetime.timedelta(seconds=DURATION)
while end > datetime.datetime.now():

    # Take a snapshot and mark the snapshot time.
    hello, image = cap.read()

    # Compute alpha value.
    alpha, tstamp_prev = iproc.getAlpha(tstamp_prev)

    # Preprocess the image.
    image_pre = iproc.preprocess2(image)

    # Initalize accumulation if so indicated.
    if image_acc is None:
        image_acc = np.empty(np.shape(image_pre))

    # Compute difference.
    image_diff = cv2.absdiff(
        image_acc.astype(image_pre.dtype),
        image_pre,
        )

    # Accumulate.
    hello = cv2.accumulateWeighted(
        image_pre,
        image_acc,
        alpha,
        )

    # Draw the difference on top of the image.
    iproc.postprocess(image, image_diff)

    # Write the framerate on top of the image.
    iproc.writeOSD(
        image,
        ('%.2f, %.2f, %.2f fps'%framerate.tick(),),
        ratio=0.04,
        )

    # Display the image.
    cv2.imshow('motion 1', image)

    # Allow HighGUI to process event.
    cv2.waitKey(1)

# The end.
<|endoftext|>


<commit_before># Copyright (C) 2013, Thomas Leonard
# See the README file for details, or visit http://0install.net.

from __future__ import print_function

import os, subprocess
from os.path import join, dirname, abspath

from zeroinstall import SafeException

from repo import scm

topdir = dirname(dirname(dirname(abspath(__file__))))

def handle(args):
	# Get the fingerprint from the key ID (and check we have the secret key)
	try:
		keys = subprocess.check_output(['gpg', '-q', '--fixed-list-mode', '--fingerprint', '--with-colons', '--list-secret-keys', args.key])
	except subprocess.CalledProcessError as ex:
		raise SafeException("GPG key '{key}' not found ({ex})".format(key = args.key, ex = ex))

	fingerprint = None
	for line in keys.split('\n'):
		bits = line.split(':')
		if bits[0] == 'fpr':
			if fingerprint is None:
				fingerprint = bits[9]
			else:
				raise SafeException("Multiple GPG keys match '{key}':\n{output}".format(
					key = args.key, output = keys))

	if fingerprint is None:
		raise SafeException("GPG key not found '{key}'".format(key = args.key))

	# Create the directory structure
	os.mkdir(args.path)
	os.chdir(args.path)
	os.mkdir('incoming')
	os.mkdir('feeds')
	os.mkdir('public')

	# Write the configuration file, with the GPG key filled in
	with open(join(topdir, 'resources', '0repo-config.py.template'), 'rt') as stream:
		data = stream.read()
	data = data.replace('{{GPGKEY}}', '0x' + fingerprint)
	with open('0repo-config.py', 'wt') as stream:
		stream.write(data)

	# Initialise the Git repository
	subprocess.check_call(['git', 'init', '-q', 'feeds'])
	scm.commit('feeds', [], 'Created new repository', '0x' + fingerprint, extra_options = ['--allow-empty'])
<commit_msg>Ignore subkeys when creating a repository<commit_after># Copyright (C) 2013, Thomas Leonard
# See the README file for details, or visit http://0install.net.

from __future__ import print_function

import os, subprocess
from os.path import join, dirname, abspath

from zeroinstall import SafeException

from repo import scm

topdir = dirname(dirname(dirname(abspath(__file__))))

def handle(args):
	# Get the fingerprint from the key ID (and check we have the secret key)
	try:
		keys = subprocess.check_output(['gpg', '-q', '--fixed-list-mode', '--fingerprint', '--with-colons', '--list-secret-keys', args.key])
	except subprocess.CalledProcessError as ex:
		raise SafeException("GPG key '{key}' not found ({ex})".format(key = args.key, ex = ex))

	in_ssb = False
	fingerprint = None
	for line in keys.split('\n'):
		bits = line.split(':')
		if bits[0] == 'ssb': in_ssb = True
		elif bits[0] == 'sec': in_ssb = False
		elif bits[0] == 'fpr':
			if in_ssb and fingerprint is not None:
				pass	# Ignore sub-keys (unless we don't have a primary - can that happen?)
			elif fingerprint is None:
				fingerprint = bits[9]
			else:
				raise SafeException("Multiple GPG keys match '{key}':\n{output}".format(
					key = args.key, output = keys))

	if fingerprint is None:
		raise SafeException("GPG key not found '{key}'".format(key = args.key))

	# Create the directory structure
	os.mkdir(args.path)
	os.chdir(args.path)
	os.mkdir('incoming')
	os.mkdir('feeds')
	os.mkdir('public')

	# Write the configuration file, with the GPG key filled in
	with open(join(topdir, 'resources', '0repo-config.py.template'), 'rt') as stream:
		data = stream.read()
	data = data.replace('{{GPGKEY}}', '0x' + fingerprint)
	with open('0repo-config.py', 'wt') as stream:
		stream.write(data)

	# Initialise the Git repository
	subprocess.check_call(['git', 'init', '-q', 'feeds'])
	scm.commit('feeds', [], 'Created new repository', '0x' + fingerprint, extra_options = ['--allow-empty'])
<|endoftext|>


<commit_before># Needs id3reader: http://nedbatchelder.com/code/modules/id3reader.py
#
# TODO: command-line arguments:
# * mp3name.exe [c:\dir\to\scan] [-i] [-c] [-f]
# * Dir to scan = defaults to current dir
# * -i = Invert artist (e.g. 'The Beatles' = 'Beatles, The'
# * -c = Capitalize Words (default uses tag data)
# * -f = Sort into artist/album folders
import os, re, id3reader

for file in os.listdir('.' + os.sep):
    ext = os.path.splitext(file)

    if ext[1] == '.mp3':
        # Construct a reader from a file or filename.
        id3r = id3reader.Reader(file)

        #Artist, album, title:
        artist = id3r.getValue('performer') + ' - ' if(len(id3r.getValue('performer')) != 0) else ''
        album  = id3r.getValue('album')     + ' - ' if(len(id3r.getValue('album'))     != 0) else ''
        title  = id3r.getValue('title')             if(len(id3r.getValue('title'))     != 0) else ext[0]

	# Sometimes track is stored like 06/10, we just want the 06:
        track = re.sub('/\d+', '', id3r.getValue('track'))

	# TODO: Somehow get max len() of track? (e.g. 01 vs 001)
        if len(track) == 0:
            track = ''
        elif len(track) == 1:
            track = '0' + track + ' - '
        elif len(track) == 2:
            track = track + ' - '

        filename = re.sub('[\\/:\*\?"<>\|]', '', artist + album + track + title + '.mp3')

        print file, ' -> ', filename	#print filename
        os.rename(file, filename)
<commit_msg>Edited mp3name.py via GitHub<commit_after># Needs id3reader: http://nedbatchelder.com/code/modules/id3reader.py
#
# TODO: command-line arguments:

import os, re, id3reader

for file in os.listdir('.' + os.sep):
    ext = os.path.splitext(file)

    if ext[1] == '.mp3':
        # Construct a reader from a file or filename.
        id3r = id3reader.Reader(file)

        #Artist, album, title:
        artist = id3r.getValue('performer') + ' - ' if(len(id3r.getValue('performer')) != 0) else ''
        album  = id3r.getValue('album')     + ' - ' if(len(id3r.getValue('album'))     != 0) else ''
        title  = id3r.getValue('title')             if(len(id3r.getValue('title'))     != 0) else ext[0]

	# Sometimes track is stored like 06/10, we just want the 06:
        track = re.sub('/\d+', '', id3r.getValue('track'))

	# TODO: Somehow get max len() of track? (e.g. 01 vs 001)
        if len(track) == 0:
            track = ''
        elif len(track) == 1:
            track = '0' + track + ' - '
        elif len(track) == 2:
            track = track + ' - '

        filename = re.sub('[\\/:\*\?"<>\|]', '', artist + album + track + title + '.mp3')

        print file, ' -> ', filename	#print filename
        os.rename(file, filename)
<|endoftext|>


<commit_before>from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError
from django.test import TestCase, RequestFactory

from mock import Mock, patch

from .reporters import ModelReporter
from .utils import UnicodeWriter
from .views import BaseCSVGeneratorView

class ModelReporterMetaclassTestCase(TestCase):

    def test_select_invalid_fields(self):
        with self.assertRaises(FieldError) as exception:
            class ThisShouldFail(ModelReporter):
                class Meta:
                    model = Permission
                    fields = ('foo', 'span', 'codename')

        self.assertEqual(
            exception.exception.message,
            'Unknown field(s) (foo, span) specified for Permission'
        )

    def test_invalid_headers(self):
        with self.assertRaises(FieldError) as exception:
            class ThisShouldFail(ModelReporter):
                class Meta:
                    model = Permission
                    custom_headers = {
                        'foo': 'Foo', 'span': 'Span', 'codename': 'Meh'
                    }

        self.assertEqual(
            exception.exception.message,
            'Unknown header(s) (foo, span) specified for Permission'
        )

    def test_custom_header_with_non_selected_field(self):
        with self.assertRaises(FieldError) as exception:
            class ThisShouldFail(ModelReporter):
                class Meta:
                    model = Permission
                    fields = ('name',)
                    custom_headers = {'codename': 'Meh'}

        self.assertEqual(
            exception.exception.message,
            'Unknown header(s) (codename) specified for Permission'
        )

# Test classes
class BaseUserReporter(ModelReporter):
    class Meta:
        model = get_user_model()

class PermissionReporterWithAllFields(ModelReporter):
    class Meta:
        model = Permission

class PermissionReporterWithSomeFields(ModelReporter):
    class Meta:
        model = Permission
        fields = ('name', 'codename')

class PermissionReporterWithSomeFieldsAndCustomRenderer(ModelReporter):
    class Meta:
        model = Permission
        fields = ('name', 'codename')

    def render_codename(self, instance):
        return instance.codename.replace('_', ' ').capitalize()

class PermissionReporterWithCustomHeaders(ModelReporter):
    class Meta:
        model = Permission
        custom_headers = {
            'id': 'Key',
            'name': 'Foo',
        }

class GroupReporter(ModelReporter):
    class Meta:
        model = Group
        fields = ('name', 'permissions',)

class ModelReporterTestCase(TestCase):

    def _create_users(self, _quantity=5):
        for i in range(1, _quantity + 1):
            username = 'foo%s' % i
            email = '%s@example.com' % i

            get_user_model().objects.create(username=username, email=email)

    def test_basic_reporter(self):
        reporter = BaseUserReporter()

        self.assertEqual(reporter.items.count(), 0)

    def test_reporter_with_some_items(self):
        self._create_users(_quantity=5)
        reporter = BaseUserReporter()

        self.assertEqual(reporter.items.count(), 5)

    def test_reporter_with_fixed_queryset(self):
        self._create_users(_quantity=10)
        reporter = BaseUserReporter(get_user_model().objects.all()[:7])

        self.assertEqual(reporter.items.count(), 7)

    def test_reporter_gets_all_model_fields(self):
        reporter = PermissionReporterWithAllFields()

        self.assertEqual(
            set(reporter.fields),
            set(['codename', 'content_type', 'group', u'id', 'name', 'user'])
        )

    def test_reporter_gets_given_model_fields(self):
        reporter = PermissionReporterWithSomeFields()

        self.assertEqual(
            reporter.fields,
            ('name', 'codename')
        )

    def test_default_headers(self):
        reporter = PermissionReporterWithAllFields()

        self.assertEqual(
            set(reporter.rendered_headers()),
            set([u'Codename', u'Content type', 'Group', u'Id', u'Name', 'User'])
        )

    def test_custom_headers(self):
        reporter = PermissionReporterWithCustomHeaders()

        self.assertEqual(
            set(reporter.rendered_headers()),
            set([u'Codename', u'Content type', 'Group', u'Key', u'Foo', 'User'])
        )

    def test_row_generation_with_all_fields(self):
        ct = ContentType.objects.get_for_model(Permission)
        permissions = Permission.objects.filter(content_type=ct)

        reporter = PermissionReporterWithAllFields(permissions)
        permission = permissions.get(codename='add_permission')

        self.assertEqual(
            reporter.rendered_fields(permission),
            {
                'codename': u'add_permission', 'content_type': u'permission',
                'group': u'', u'id': u'1',
                'name': u'Can add permission', 'user': u'',
            }
        )

    def test_generate_all_rows_with_all_fields(self):
        ct = ContentType.objects.get_for_model(Permission)
        permissions = Permission.objects.filter(content_type=ct)

        reporter = PermissionReporterWithAllFields(permissions)

        self.assertEqual(
            [row for row in reporter.rendered_rows()],
            [
                [u'add_permission', u'permission', u'', u'1', u'Can add permission', u''],
                [u'change_permission', u'permission', u'', u'2', u'Can change permission', u''],
                [u'delete_permission', u'permission', u'', u'3', u'Can delete permission', u''],
            ]
        )

    def test_row_generation_with_some_fields(self):
        ct = ContentType.objects.get_for_model(Permission)
        permissions = Permission.objects.filter(content_type=ct)

        reporter = PermissionReporterWithSomeFields(permissions)
        permission = permissions.get(codename='add_permission')

        self.assertEqual(
            reporter.rendered_fields(permission),
            {'codename': 'add_permission', 'name': 'Can add permission'}
        )

    def test_generate_all_rows_with_some_fields(self):
        ct = ContentType.objects.get_for_model(Permission)
        permissions = Permission.objects.filter(content_type=ct)

        reporter = PermissionReporterWithSomeFields(permissions)

        self.assertEqual(
            [row for row in reporter.rendered_rows()],
            [
                ['Can add permission', 'add_permission'],
                ['Can change permission', 'change_permission'],
                ['Can delete permission', 'delete_permission'],
            ]
        )

    def test_many_to_many_fields(self):
        ct = ContentType.objects.get_for_model(Permission)
        permissions = Permission.objects.filter(content_type=ct)

        self.assertEqual(Group.objects.count(), 0)
        group = Group.objects.create(name='foo')
        group.permissions.add(*permissions)

        reporter = GroupReporter()

        self.assertEqual(
            [row for row in reporter.rendered_rows()],
            [
                [u'foo', u'auth | permission | Can add permission, auth | permission | Can change permission, auth | permission | Can delete permission'],
            ]
        )

    def test_custom_renderer(self):
        ct = ContentType.objects.get_for_model(Permission)
        permissions = Permission.objects.filter(content_type=ct)

        reporter = PermissionReporterWithSomeFieldsAndCustomRenderer(permissions)

        self.assertEqual(
            [row for row in reporter.rendered_rows()],
            [
                ['Can add permission', 'Add permission'],
                ['Can change permission', 'Change permission'],
                ['Can delete permission', 'Delete permission'],
            ]
        )

class BaseCSVGeneratorViewTestCase(TestCase):

    def test_get_reporter_class(self):
        view = BaseCSVGeneratorView()
        mock = Mock()
        view.reporter_class = mock

        self.assertEqual(view.get_reporter_class(), mock)

    def test_get_reporter(self):
        view = BaseCSVGeneratorView()
        mock = Mock()
        mock.return_value = 'foo'  # we make sure we're instantiating the class
        view.reporter_class = mock
        view.get_queryset = lambda: [1, 2, 3]

        self.assertEqual(view.get_reporter(), 'foo')
        mock.assert_called_once_with([1, 2, 3])

    def test_get_writter_class_default(self):
        view = BaseCSVGeneratorView()

        self.assertEqual(view.get_writer_class(), UnicodeWriter)

    def test_get_writter_class(self):
        view = BaseCSVGeneratorView()
        mock = Mock()
        view.writer_class = mock

        self.assertEqual(view.get_writer_class(), mock)

    def test_should_write_header(self):
        view = BaseCSVGeneratorView()
        self.assertTrue(view.should_write_header())

        view.WRITE_HEADER = False
        self.assertFalse(view.should_write_header())

    def test_get_file_name_default(self):
        view = BaseCSVGeneratorView()

        self.assertEqual(view.get_file_name(), 'myreport.csv')

    def test_get_file_name_default(self):
        view = BaseCSVGeneratorView()
        view.file_name = 'kinginthenorth.csv'

        self.assertEqual(view.get_file_name(), 'kinginthenorth.csv')

    def test_get_should_generate_csv(self):
        view = BaseCSVGeneratorView()
        view.write_csv = Mock()
        request = RequestFactory().get('/')

        with patch('reportato.views.HttpResponse') as http_response_patch:
            http_response_patch.return_value = {}

            response = view.get(request)

        self.assertEqual(
            response,
            {'Content-Disposition': 'attachment; filename="myreport.csv"'}
        )
        view.write_csv.assert_called_once()

    def test_write_csv_with_header(self):
        view = BaseCSVGeneratorView()
        writer_mock = Mock()
        reporter_mock = Mock()

        view.get_writer_class = lambda: writer_mock
        view.get_reporter = lambda: reporter_mock

        view.write_csv(Mock())

        # check we rendered the headers and the rows
        reporter_mock.rendered_headers.assert_called_once()
        reporter_mock.rendered_rows.assert_called_once()

        # and that we wrote those things
        writer_mock.return_value.writerow.assert_called_once_with(
            reporter_mock.rendered_headers())
        writer_mock.return_value.writerows.assert_called_once_with(
            reporter_mock.rendered_rows())

    def test_write_csv_without_header(self):
        view = BaseCSVGeneratorView()
        writer_mock = Mock()
        reporter_mock = Mock()

        view.get_writer_class = lambda: writer_mock
        view.get_reporter = lambda: reporter_mock
        view.WRITE_HEADER = False

        view.write_csv(Mock())

        # check we rendered the headers and the rows
        self.assertFalse(reporter_mock.rendered_headers.called)
        reporter_mock.rendered_rows.assert_called_once()

        # and that we wrote those things
        self.assertFalse(writer_mock.return_value.writerow.called)
        writer_mock.return_value.writerows.assert_called_once_with(
            reporter_mock.rendered_rows())
<commit_msg>Typo on the test<commit_after>from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError
from django.test import TestCase, RequestFactory

from mock import Mock, patch

from .reporters import ModelReporter
from .utils import UnicodeWriter
from .views import BaseCSVGeneratorView

class ModelReporterMetaclassTestCase(TestCase):

    def test_select_invalid_fields(self):
        with self.assertRaises(FieldError) as exception:
            class ThisShouldFail(ModelReporter):
                class Meta:
                    model = Permission
                    fields = ('foo', 'span', 'codename')

        self.assertEqual(
            exception.exception.message,
            'Unknown field(s) (foo, span) specified for Permission'
        )

    def test_invalid_headers(self):
        with self.assertRaises(FieldError) as exception:
            class ThisShouldFail(ModelReporter):
                class Meta:
                    model = Permission
                    custom_headers = {
                        'foo': 'Foo', 'span': 'Span', 'codename': 'Meh'
                    }

        self.assertEqual(
            exception.exception.message,
            'Unknown header(s) (foo, span) specified for Permission'
        )

    def test_custom_header_with_non_selected_field(self):
        with self.assertRaises(FieldError) as exception:
            class ThisShouldFail(ModelReporter):
                class Meta:
                    model = Permission
                    fields = ('name',)
                    custom_headers = {'codename': 'Meh'}

        self.assertEqual(
            exception.exception.message,
            'Unknown header(s) (codename) specified for Permission'
        )

# Test classes
class BaseUserReporter(ModelReporter):
    class Meta:
        model = get_user_model()

class PermissionReporterWithAllFields(ModelReporter):
    class Meta:
        model = Permission

class PermissionReporterWithSomeFields(ModelReporter):
    class Meta:
        model = Permission
        fields = ('name', 'codename')

class PermissionReporterWithSomeFieldsAndCustomRenderer(ModelReporter):
    class Meta:
        model = Permission
        fields = ('name', 'codename')

    def render_codename(self, instance):
        return instance.codename.replace('_', ' ').capitalize()

class PermissionReporterWithCustomHeaders(ModelReporter):
    class Meta:
        model = Permission
        custom_headers = {
            'id': 'Key',
            'name': 'Foo',
        }

class GroupReporter(ModelReporter):
    class Meta:
        model = Group
        fields = ('name', 'permissions',)

class ModelReporterTestCase(TestCase):

    def _create_users(self, _quantity=5):
        for i in range(1, _quantity + 1):
            username = 'foo%s' % i
            email = '%s@example.com' % i

            get_user_model().objects.create(username=username, email=email)

    def test_basic_reporter(self):
        reporter = BaseUserReporter()

        self.assertEqual(reporter.items.count(), 0)

    def test_reporter_with_some_items(self):
        self._create_users(_quantity=5)
        reporter = BaseUserReporter()

        self.assertEqual(reporter.items.count(), 5)

    def test_reporter_with_fixed_queryset(self):
        self._create_users(_quantity=10)
        reporter = BaseUserReporter(get_user_model().objects.all()[:7])

        self.assertEqual(reporter.items.count(), 7)

    def test_reporter_gets_all_model_fields(self):
        reporter = PermissionReporterWithAllFields()

        self.assertEqual(
            set(reporter.fields),
            set(['codename', 'content_type', 'group', u'id', 'name', 'user'])
        )

    def test_reporter_gets_given_model_fields(self):
        reporter = PermissionReporterWithSomeFields()

        self.assertEqual(
            reporter.fields,
            ('name', 'codename')
        )

    def test_default_headers(self):
        reporter = PermissionReporterWithAllFields()

        self.assertEqual(
            set(reporter.rendered_headers()),
            set([u'Codename', u'Content type', 'Group', u'Id', u'Name', 'User'])
        )

    def test_custom_headers(self):
        reporter = PermissionReporterWithCustomHeaders()

        self.assertEqual(
            set(reporter.rendered_headers()),
            set([u'Codename', u'Content type', 'Group', u'Key', u'Foo', 'User'])
        )

    def test_row_generation_with_all_fields(self):
        ct = ContentType.objects.get_for_model(Permission)
        permissions = Permission.objects.filter(content_type=ct)

        reporter = PermissionReporterWithAllFields(permissions)
        permission = permissions.get(codename='add_permission')

        self.assertEqual(
            reporter.rendered_fields(permission),
            {
                'codename': u'add_permission', 'content_type': u'permission',
                'group': u'', u'id': u'1',
                'name': u'Can add permission', 'user': u'',
            }
        )

    def test_generate_all_rows_with_all_fields(self):
        ct = ContentType.objects.get_for_model(Permission)
        permissions = Permission.objects.filter(content_type=ct)

        reporter = PermissionReporterWithAllFields(permissions)

        self.assertEqual(
            [row for row in reporter.rendered_rows()],
            [
                [u'add_permission', u'permission', u'', u'1', u'Can add permission', u''],
                [u'change_permission', u'permission', u'', u'2', u'Can change permission', u''],
                [u'delete_permission', u'permission', u'', u'3', u'Can delete permission', u''],
            ]
        )

    def test_row_generation_with_some_fields(self):
        ct = ContentType.objects.get_for_model(Permission)
        permissions = Permission.objects.filter(content_type=ct)

        reporter = PermissionReporterWithSomeFields(permissions)
        permission = permissions.get(codename='add_permission')

        self.assertEqual(
            reporter.rendered_fields(permission),
            {'codename': 'add_permission', 'name': 'Can add permission'}
        )

    def test_generate_all_rows_with_some_fields(self):
        ct = ContentType.objects.get_for_model(Permission)
        permissions = Permission.objects.filter(content_type=ct)

        reporter = PermissionReporterWithSomeFields(permissions)

        self.assertEqual(
            [row for row in reporter.rendered_rows()],
            [
                ['Can add permission', 'add_permission'],
                ['Can change permission', 'change_permission'],
                ['Can delete permission', 'delete_permission'],
            ]
        )

    def test_many_to_many_fields(self):
        ct = ContentType.objects.get_for_model(Permission)
        permissions = Permission.objects.filter(content_type=ct)

        self.assertEqual(Group.objects.count(), 0)
        group = Group.objects.create(name='foo')
        group.permissions.add(*permissions)

        reporter = GroupReporter()

        self.assertEqual(
            [row for row in reporter.rendered_rows()],
            [
                [u'foo', u'auth | permission | Can add permission, auth | permission | Can change permission, auth | permission | Can delete permission'],
            ]
        )

    def test_custom_renderer(self):
        ct = ContentType.objects.get_for_model(Permission)
        permissions = Permission.objects.filter(content_type=ct)

        reporter = PermissionReporterWithSomeFieldsAndCustomRenderer(permissions)

        self.assertEqual(
            [row for row in reporter.rendered_rows()],
            [
                ['Can add permission', 'Add permission'],
                ['Can change permission', 'Change permission'],
                ['Can delete permission', 'Delete permission'],
            ]
        )

class BaseCSVGeneratorViewTestCase(TestCase):

    def test_get_reporter_class(self):
        view = BaseCSVGeneratorView()
        mock = Mock()
        view.reporter_class = mock

        self.assertEqual(view.get_reporter_class(), mock)

    def test_get_reporter(self):
        view = BaseCSVGeneratorView()
        mock = Mock()
        mock.return_value = 'foo'  # we make sure we're instantiating the class
        view.reporter_class = mock
        view.get_queryset = lambda: [1, 2, 3]

        self.assertEqual(view.get_reporter(), 'foo')
        mock.assert_called_once_with([1, 2, 3])

    def test_get_writter_class_default(self):
        view = BaseCSVGeneratorView()

        self.assertEqual(view.get_writer_class(), UnicodeWriter)

    def test_get_writter_class(self):
        view = BaseCSVGeneratorView()
        mock = Mock()
        view.writer_class = mock

        self.assertEqual(view.get_writer_class(), mock)

    def test_should_write_header(self):
        view = BaseCSVGeneratorView()
        self.assertTrue(view.should_write_header())

        view.WRITE_HEADER = False
        self.assertFalse(view.should_write_header())

    def test_get_file_name_default(self):
        view = BaseCSVGeneratorView()

        self.assertEqual(view.get_file_name(), 'myreport.csv')

    def test_get_file_name_non_default(self):
        view = BaseCSVGeneratorView()
        view.file_name = 'kinginthenorth.csv'

        self.assertEqual(view.get_file_name(), 'kinginthenorth.csv')

    def test_get_should_generate_csv(self):
        view = BaseCSVGeneratorView()
        view.write_csv = Mock()
        request = RequestFactory().get('/')

        with patch('reportato.views.HttpResponse') as http_response_patch:
            http_response_patch.return_value = {}

            response = view.get(request)

        self.assertEqual(
            response,
            {'Content-Disposition': 'attachment; filename="myreport.csv"'}
        )
        view.write_csv.assert_called_once()

    def test_write_csv_with_header(self):
        view = BaseCSVGeneratorView()
        writer_mock = Mock()
        reporter_mock = Mock()

        view.get_writer_class = lambda: writer_mock
        view.get_reporter = lambda: reporter_mock

        view.write_csv(Mock())

        # check we rendered the headers and the rows
        reporter_mock.rendered_headers.assert_called_once()
        reporter_mock.rendered_rows.assert_called_once()

        # and that we wrote those things
        writer_mock.return_value.writerow.assert_called_once_with(
            reporter_mock.rendered_headers())
        writer_mock.return_value.writerows.assert_called_once_with(
            reporter_mock.rendered_rows())

    def test_write_csv_without_header(self):
        view = BaseCSVGeneratorView()
        writer_mock = Mock()
        reporter_mock = Mock()

        view.get_writer_class = lambda: writer_mock
        view.get_reporter = lambda: reporter_mock
        view.WRITE_HEADER = False

        view.write_csv(Mock())

        # check we rendered the headers and the rows
        self.assertFalse(reporter_mock.rendered_headers.called)
        reporter_mock.rendered_rows.assert_called_once()

        # and that we wrote those things
        self.assertFalse(writer_mock.return_value.writerow.called)
        writer_mock.return_value.writerows.assert_called_once_with(
            reporter_mock.rendered_rows())
<|endoftext|>


<commit_before># coding: utf-8
import collections
from typing import Callable, Dict, Optional, List, Set, Any, Tuple

from siebenapp.domain import Graph

class Edge(collections.namedtuple('Edge', 'target type')):
    TYPE_SOFT = 1
    TYPE_STRONG = 2
    __slots__ = ()

    @classmethod
    def build(cls, target, link_type):
        return Edge(target, link_type)

    @classmethod
    def soft(cls, target):
        return Edge(target, Edge.TYPE_SOFT)

    @classmethod
    def strong(cls, target):
        return Edge(target, Edge.TYPE_STRONG)

GoalsData = List[Tuple[int, Optional[str], bool]]
EdgesData = List[Tuple[int, int, int]]
OptionsData = List[Tuple[str, int]]

class Goals(Graph):
    def __init__(self, name: str, message_fn: Callable[[str], None] = None) -> None:
        self.goals = {}  # type: Dict[int, Optional[str]]
        self.edges = {}  # type: Dict[int, List[Edge]]
        self.back_edges = {}  # type: Dict[int, List[int]]
        self.closed = set()  # type: Set[int]
        self.settings = {
            'selection': 1,
            'previous_selection': 1,
        }  # type: Dict[str, int]
        self.events = collections.deque()  # type: collections.deque
        self.message_fn = message_fn
        self._add_no_link(name)

    def _msg(self, message: str) -> None:
        if self.message_fn:
            self.message_fn(message)

    def _has_link(self, lower: int, upper: int) -> bool:
        return upper in set(x.target for x in self.edges[lower])

    def add(self, name: str, add_to: int = 0, edge_type: int = Edge.TYPE_STRONG) -> bool:
        if add_to == 0:
            add_to = self.settings['selection']
        if add_to in self.closed:
            self._msg("A new subgoal cannot be added to the closed one")
            return False
        next_id = self._add_no_link(name)
        self.toggle_link(add_to, next_id, edge_type)
        return True

    def _add_no_link(self, name: str) -> int:
        next_id = max(list(self.goals.keys()) + [0]) + 1
        self.goals[next_id] = name
        self.edges[next_id] = list()
        self.back_edges[next_id] = list()
        self.events.append(('add', next_id, name, True))
        return next_id

    def select(self, goal_id: int) -> None:
        if goal_id in self.goals and self.goals[goal_id] is not None:
            self.settings['selection'] = goal_id
            self.events.append(('select', goal_id))

    def hold_select(self) -> None:
        self.settings['previous_selection'] = self.settings['selection']
        self.events.append(('hold_select', self.settings['selection']))

    def q(self, keys: str = 'name') -> Dict[int, Any]:
        def sel(x):
            # type: (int) -> Optional[str]
            if x == self.settings['selection']:
                return 'select'
            elif x == self.settings['previous_selection']:
                return 'prev'
            return None

        keys_list = keys.split(',')
        result = dict()  # type: Dict[int, Any]
        for key, name in ((k, n) for k, n in self.goals.items() if n is not None):
            value = {
                'edge': sorted([(x.target, x.type) for x in self.edges[key]]),
                'name': name,
                'open': key not in self.closed,
                'select': sel(key),
                'switchable': self._switchable(key),
            }
            result[key] = {k: v for k, v in value.items() if k in keys_list}
        return result

    def _switchable(self, key: int) -> bool:
        if key in self.closed:
            has_open_parents = len(set(self.back_edges[key]).difference(self.closed)) > 0
            has_no_parents = not self.back_edges[key]
            return has_open_parents or has_no_parents
        else:
            return all(x.target in self.closed for x in self.edges[key])

    def insert(self, name: str) -> None:
        lower = self.settings['previous_selection']
        upper = self.settings['selection']
        if lower == upper:
            self._msg("A new goal can be inserted only between two different goals")
            return
        edge_type = Edge.TYPE_SOFT
        for edge in self.edges[lower]:
            if edge.target == upper:
                edge_type = edge.type
                break
        if self.add(name, lower, edge_type):
            key = len(self.goals)
            self.toggle_link(key, upper, edge_type)
            if self._has_link(lower, upper):
                self.toggle_link(lower, upper, edge_type)

    def rename(self, new_name: str, goal_id: int = 0) -> None:
        if goal_id == 0:
            goal_id = self.settings['selection']
        self.goals[goal_id] = new_name
        self.events.append(('rename', new_name, goal_id))

    def toggle_close(self) -> None:
        if self.settings['selection'] in self.closed:
            if self._may_be_reopened():
                self.closed.remove(self.settings['selection'])
                self.events.append(('toggle_close', True, self.settings['selection']))
            else:
                self._msg("This goal can't be reopened because other subgoals block it")
        else:
            if self._may_be_closed():
                self.closed.add(self.settings['selection'])
                self.events.append(('toggle_close', False, self.settings['selection']))
                self.select(1)
                self.hold_select()
            else:
                self._msg("This goal can't be closed because it have open subgoals")

    def _may_be_closed(self) -> bool:
        return all(g.target in self.closed for g in self.edges[self.settings['selection']])

    def _may_be_reopened(self) -> bool:
        parent_goals = self.back_edges[self.settings['selection']]
        return all(g not in self.closed for g in parent_goals)

    def delete(self, goal_id: int = 0) -> None:
        if goal_id == 0:
            goal_id = self.settings['selection']
        if goal_id == 1:
            self._msg("Root goal can't be deleted")
            return
        self._delete(goal_id)
        self.select(1)
        self.hold_select()

    def _delete(self, goal_id: int) -> None:
        self.goals[goal_id] = None
        self.closed.add(goal_id)
        next_to_remove = self.edges.pop(goal_id, [])  # type: List[Edge]
        self.back_edges.pop(goal_id, {})
        for key, values in self.edges.items():
            self.edges[key] = [x for x in values if x.target != goal_id]
        for key in self.back_edges:
            if goal_id in self.back_edges[key]:
                self.back_edges[key].remove(goal_id)
        for next_goal in next_to_remove:
            if not self.back_edges.get(next_goal.target, []):
                self._delete(next_goal.target)
        self.events.append(('delete', goal_id))

    def toggle_link(self, lower: int = 0, upper: int = 0, edge_type: int = Edge.TYPE_SOFT) -> None:
        lower = self.settings['previous_selection'] if lower == 0 else lower
        upper = self.settings['selection'] if upper == 0 else upper
        if lower == upper:
            self._msg("Goal can't be linked to itself")
            return
        if self._has_link(lower, upper):
            current_edge_type = None
            for edge in self.edges[lower]:
                if edge.target == upper:
                    current_edge_type = edge.type
                    break
            if current_edge_type != edge_type:
                self._create_new_link(lower, upper, edge_type)
            self._remove_existing_link(lower, upper, current_edge_type)
        else:
            self._create_new_link(lower, upper, edge_type)

    def _remove_existing_link(self, lower: int, upper: int, edge_type=None) -> None:
        edges_to_upper = len(self.back_edges[upper])
        if edges_to_upper > 1:
            self.edges[lower].remove(Edge.build(upper, edge_type))
            self.back_edges[upper].remove(lower)
            self.events.append(('unlink', lower, upper, edge_type))
        else:
            self._msg("Can't remove the last link")

    def _create_new_link(self, lower: int, upper: int, edge_type: int) -> None:
        if lower in self.closed and upper not in self.closed:
            self._msg("An open goal can't block already closed one")
            return
        front = {upper}  # type: Set[int]
        visited = set()  # type: Set[int]
        total = set()  # type: Set[int]
        while front:
            g = front.pop()
            visited.add(g)
            for e in self.edges[g]:
                total.add(e.target)
                if e not in visited:
                    front.add(e.target)
        if lower not in total:
            edge = Edge.build(upper, edge_type)
            self.edges[lower].append(edge)
            self.back_edges[upper].append(lower)
            self.events.append(('link', lower, upper, edge.type))
        else:
            self._msg("Circular dependencies between goals are not allowed")

    def verify(self) -> bool:
        assert all(g.target in self.closed for p in self.closed for g in self.edges.get(p, [])), \
            'Open goals could not be blocked by closed ones'

        queue = [1]  # type: List[int]
        visited = set()  # type: Set[int]
        while queue:
            goal = queue.pop()
            queue.extend(g.target for g in self.edges[goal]
                         if g.target not in visited and self.goals[g.target] is not None)
            visited.add(goal)
        assert visited == set(x for x in self.goals if self.goals[x] is not None), \
            'All subgoals must be accessible from the root goal'

        deleted_nodes = [g for g, v in self.goals.items() if v is None]
        assert all(not self.edges.get(n) for n in deleted_nodes), \
            'Deleted goals must have no dependencies'

        assert all(k in self.settings for k in {'selection', 'previous_selection'})

        return True

    @staticmethod
    def build(goals, edges, settings, message_fn=None):
        # type: (GoalsData, EdgesData, OptionsData, Callable[[str], None]) -> Goals
        result = Goals('', message_fn)
        result.events.clear()  # remove initial goal
        goals_dict = dict((g[0], g[1]) for g in goals)
        result.goals = dict((i, goals_dict.get(i))
                            for i in range(1, max(goals_dict.keys()) + 1))
        result.closed = set(g[0] for g in goals if not g[2]).union(
            set(k for k, v in result.goals.items() if v is None))
        d = collections.defaultdict(list)  # type: Dict[int, List[Edge]]
        bd = collections.defaultdict(list)  # type: Dict[int, List[int]]
        for parent, child, link_type in edges:
            d[parent].append(Edge.build(child, link_type))
            bd[child].append(parent)
        result.edges = dict(d)
        result.back_edges = dict(bd)
        result.edges.update(dict((g, []) for g in result.goals if g not in d))
        result.back_edges.update(dict((g, []) for g in result.goals if g not in bd))
        result.settings.update(settings)
        result.verify()
        return result

    @staticmethod
    def export(goals):
        # type: (Goals) -> Tuple[GoalsData, EdgesData, OptionsData]
        nodes = [(g_id, g_name, g_id not in goals.closed)
                 for g_id, g_name in goals.goals.items()]
        edges = [(parent, child.target, child.type) for parent in goals.edges
                 for child in goals.edges[parent]]
        settings = list(goals.settings.items())
        return nodes, edges, settings
<commit_msg>Add parent to Edge class<commit_after># coding: utf-8
import collections
from typing import Callable, Dict, Optional, List, Set, Any, Tuple

from siebenapp.domain import Graph

class Edge(collections.namedtuple('Edge', 'source target type')):
    TYPE_SOFT = 1
    TYPE_STRONG = 2
    __slots__ = ()

    @classmethod
    def build(cls, source, target, link_type):
        return Edge(source, target, link_type)

    @classmethod
    def soft(cls, source, target):
        return Edge(source, target, Edge.TYPE_SOFT)

    @classmethod
    def strong(cls, source, target):
        return Edge(source, target, Edge.TYPE_STRONG)

GoalsData = List[Tuple[int, Optional[str], bool]]
EdgesData = List[Tuple[int, int, int]]
OptionsData = List[Tuple[str, int]]

class Goals(Graph):
    def __init__(self, name: str, message_fn: Callable[[str], None] = None) -> None:
        self.goals = {}  # type: Dict[int, Optional[str]]
        self.edges = {}  # type: Dict[int, List[Edge]]
        self.back_edges = {}  # type: Dict[int, List[int]]
        self.closed = set()  # type: Set[int]
        self.settings = {
            'selection': 1,
            'previous_selection': 1,
        }  # type: Dict[str, int]
        self.events = collections.deque()  # type: collections.deque
        self.message_fn = message_fn
        self._add_no_link(name)

    def _msg(self, message: str) -> None:
        if self.message_fn:
            self.message_fn(message)

    def _has_link(self, lower: int, upper: int) -> bool:
        return upper in set(x.target for x in self.edges[lower])

    def add(self, name: str, add_to: int = 0, edge_type: int = Edge.TYPE_STRONG) -> bool:
        if add_to == 0:
            add_to = self.settings['selection']
        if add_to in self.closed:
            self._msg("A new subgoal cannot be added to the closed one")
            return False
        next_id = self._add_no_link(name)
        self.toggle_link(add_to, next_id, edge_type)
        return True

    def _add_no_link(self, name: str) -> int:
        next_id = max(list(self.goals.keys()) + [0]) + 1
        self.goals[next_id] = name
        self.edges[next_id] = list()
        self.back_edges[next_id] = list()
        self.events.append(('add', next_id, name, True))
        return next_id

    def select(self, goal_id: int) -> None:
        if goal_id in self.goals and self.goals[goal_id] is not None:
            self.settings['selection'] = goal_id
            self.events.append(('select', goal_id))

    def hold_select(self) -> None:
        self.settings['previous_selection'] = self.settings['selection']
        self.events.append(('hold_select', self.settings['selection']))

    def q(self, keys: str = 'name') -> Dict[int, Any]:
        def sel(x):
            # type: (int) -> Optional[str]
            if x == self.settings['selection']:
                return 'select'
            elif x == self.settings['previous_selection']:
                return 'prev'
            return None

        keys_list = keys.split(',')
        result = dict()  # type: Dict[int, Any]
        for key, name in ((k, n) for k, n in self.goals.items() if n is not None):
            value = {
                'edge': sorted([(x.target, x.type) for x in self.edges[key]]),
                'name': name,
                'open': key not in self.closed,
                'select': sel(key),
                'switchable': self._switchable(key),
            }
            result[key] = {k: v for k, v in value.items() if k in keys_list}
        return result

    def _switchable(self, key: int) -> bool:
        if key in self.closed:
            has_open_parents = len(set(self.back_edges[key]).difference(self.closed)) > 0
            has_no_parents = not self.back_edges[key]
            return has_open_parents or has_no_parents
        else:
            return all(x.target in self.closed for x in self.edges[key])

    def insert(self, name: str) -> None:
        lower = self.settings['previous_selection']
        upper = self.settings['selection']
        if lower == upper:
            self._msg("A new goal can be inserted only between two different goals")
            return
        edge_type = Edge.TYPE_SOFT
        for edge in self.edges[lower]:
            if edge.target == upper:
                edge_type = edge.type
                break
        if self.add(name, lower, edge_type):
            key = len(self.goals)
            self.toggle_link(key, upper, edge_type)
            if self._has_link(lower, upper):
                self.toggle_link(lower, upper, edge_type)

    def rename(self, new_name: str, goal_id: int = 0) -> None:
        if goal_id == 0:
            goal_id = self.settings['selection']
        self.goals[goal_id] = new_name
        self.events.append(('rename', new_name, goal_id))

    def toggle_close(self) -> None:
        if self.settings['selection'] in self.closed:
            if self._may_be_reopened():
                self.closed.remove(self.settings['selection'])
                self.events.append(('toggle_close', True, self.settings['selection']))
            else:
                self._msg("This goal can't be reopened because other subgoals block it")
        else:
            if self._may_be_closed():
                self.closed.add(self.settings['selection'])
                self.events.append(('toggle_close', False, self.settings['selection']))
                self.select(1)
                self.hold_select()
            else:
                self._msg("This goal can't be closed because it have open subgoals")

    def _may_be_closed(self) -> bool:
        return all(g.target in self.closed for g in self.edges[self.settings['selection']])

    def _may_be_reopened(self) -> bool:
        parent_goals = self.back_edges[self.settings['selection']]
        return all(g not in self.closed for g in parent_goals)

    def delete(self, goal_id: int = 0) -> None:
        if goal_id == 0:
            goal_id = self.settings['selection']
        if goal_id == 1:
            self._msg("Root goal can't be deleted")
            return
        self._delete(goal_id)
        self.select(1)
        self.hold_select()

    def _delete(self, goal_id: int) -> None:
        self.goals[goal_id] = None
        self.closed.add(goal_id)
        next_to_remove = self.edges.pop(goal_id, [])  # type: List[Edge]
        self.back_edges.pop(goal_id, {})
        for key, values in self.edges.items():
            self.edges[key] = [x for x in values if x.target != goal_id]
        for key in self.back_edges:
            if goal_id in self.back_edges[key]:
                self.back_edges[key].remove(goal_id)
        for next_goal in next_to_remove:
            if not self.back_edges.get(next_goal.target, []):
                self._delete(next_goal.target)
        self.events.append(('delete', goal_id))

    def toggle_link(self, lower: int = 0, upper: int = 0, edge_type: int = Edge.TYPE_SOFT) -> None:
        lower = self.settings['previous_selection'] if lower == 0 else lower
        upper = self.settings['selection'] if upper == 0 else upper
        if lower == upper:
            self._msg("Goal can't be linked to itself")
            return
        if self._has_link(lower, upper):
            current_edge_type = None
            for edge in self.edges[lower]:
                if edge.target == upper:
                    current_edge_type = edge.type
                    break
            if current_edge_type != edge_type:
                self._create_new_link(lower, upper, edge_type)
            self._remove_existing_link(lower, upper, current_edge_type)
        else:
            self._create_new_link(lower, upper, edge_type)

    def _remove_existing_link(self, lower: int, upper: int, edge_type=None) -> None:
        edges_to_upper = len(self.back_edges[upper])
        if edges_to_upper > 1:
            self.edges[lower].remove(Edge.build(lower, upper, edge_type))
            self.back_edges[upper].remove(lower)
            self.events.append(('unlink', lower, upper, edge_type))
        else:
            self._msg("Can't remove the last link")

    def _create_new_link(self, lower: int, upper: int, edge_type: int) -> None:
        if lower in self.closed and upper not in self.closed:
            self._msg("An open goal can't block already closed one")
            return
        front = {upper}  # type: Set[int]
        visited = set()  # type: Set[int]
        total = set()  # type: Set[int]
        while front:
            g = front.pop()
            visited.add(g)
            for e in self.edges[g]:
                total.add(e.target)
                if e not in visited:
                    front.add(e.target)
        if lower not in total:
            edge = Edge.build(lower, upper, edge_type)
            self.edges[lower].append(edge)
            self.back_edges[upper].append(lower)
            self.events.append(('link', lower, upper, edge.type))
        else:
            self._msg("Circular dependencies between goals are not allowed")

    def verify(self) -> bool:
        assert all(g.target in self.closed for p in self.closed for g in self.edges.get(p, [])), \
            'Open goals could not be blocked by closed ones'

        queue = [1]  # type: List[int]
        visited = set()  # type: Set[int]
        while queue:
            goal = queue.pop()
            queue.extend(g.target for g in self.edges[goal]
                         if g.target not in visited and self.goals[g.target] is not None)
            visited.add(goal)
        assert visited == set(x for x in self.goals if self.goals[x] is not None), \
            'All subgoals must be accessible from the root goal'

        deleted_nodes = [g for g, v in self.goals.items() if v is None]
        assert all(not self.edges.get(n) for n in deleted_nodes), \
            'Deleted goals must have no dependencies'

        assert all(k in self.settings for k in {'selection', 'previous_selection'})

        return True

    @staticmethod
    def build(goals, edges, settings, message_fn=None):
        # type: (GoalsData, EdgesData, OptionsData, Callable[[str], None]) -> Goals
        result = Goals('', message_fn)
        result.events.clear()  # remove initial goal
        goals_dict = dict((g[0], g[1]) for g in goals)
        result.goals = dict((i, goals_dict.get(i))
                            for i in range(1, max(goals_dict.keys()) + 1))
        result.closed = set(g[0] for g in goals if not g[2]).union(
            set(k for k, v in result.goals.items() if v is None))
        d = collections.defaultdict(list)  # type: Dict[int, List[Edge]]
        bd = collections.defaultdict(list)  # type: Dict[int, List[int]]
        for parent, child, link_type in edges:
            d[parent].append(Edge.build(parent, child, link_type))
            bd[child].append(parent)
        result.edges = dict(d)
        result.back_edges = dict(bd)
        result.edges.update(dict((g, []) for g in result.goals if g not in d))
        result.back_edges.update(dict((g, []) for g in result.goals if g not in bd))
        result.settings.update(settings)
        result.verify()
        return result

    @staticmethod
    def export(goals):
        # type: (Goals) -> Tuple[GoalsData, EdgesData, OptionsData]
        nodes = [(g_id, g_name, g_id not in goals.closed)
                 for g_id, g_name in goals.goals.items()]
        edges = [(parent, child.target, child.type) for parent in goals.edges
                 for child in goals.edges[parent]]
        settings = list(goals.settings.items())
        return nodes, edges, settings
<|endoftext|>