<commit_before>#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Natural Language Toolkit: Interface to the Stanford Chinese Segmenter
#
# Copyright (C) 2001-2016 NLTK Project
# Author: 52nlp <52nlpcn@gmail.com>
#         Casper Lehmann-Strøm <casperlehmann@gmail.com>
#
# URL: <http://nltk.org/>
# For license information, see LICENSE.TXT

from __future__ import unicode_literals, print_function

import tempfile
import os
import json
from subprocess import PIPE

from nltk import compat
from nltk.internals import find_jar, config_java, java, _java_options

from nltk.tokenize.api import TokenizerI

_stanford_url = 'http://nlp.stanford.edu/software'

class StanfordSegmenter(TokenizerI):
    r"""
    Interface to the Stanford Segmenter
    >>> from nltk.tokenize.stanford_segmenter import StanfordSegmenter
    >>> segmenter = StanfordSegmenter(
    ...     path_to_jar="stanford-segmenter-3.6.0.jar",
    ...     path_to_slf4j = "slf4j-api.jar"
    ...     path_to_sihan_corpora_dict="./data",
    ...     path_to_model="./data/pku.gz",
    ...     path_to_dict="./data/dict-chris6.ser.gz")
    >>> sentence = u"这是斯坦福中文分词器测试"
    >>> segmenter.segment(sentence)
    >>> u'\u8fd9 \u662f \u65af\u5766\u798f \u4e2d\u6587 \u5206\u8bcd\u5668 \u6d4b\u8bd5\n'
    >>> segmenter.segment_file("test.simp.utf8")
    >>> u'\u9762\u5bf9 \u65b0 \u4e16\u7eaa \uff0c \u4e16\u754c \u5404\u56fd ...
    """

    _JAR = 'stanford-segmenter.jar'
    _SLF4J = 'slf4j-api.jar'

    def __init__(self, path_to_jar=None, path_to_slf4j=None,
            path_to_sihan_corpora_dict=None,
            path_to_model=None, path_to_dict=None,
            encoding='UTF-8', options=None,
            verbose=False, java_options='-mx2g'):
        stanford_segmenter = find_jar(
                self._JAR, path_to_jar,
                env_vars=('STANFORD_SEGMENTER',),
                searchpath=(), url=_stanford_url,
                verbose=verbose)
        slf4j = find_jar(
                self._SLF4J, path_to_slf4j,
                env_vars=('SLF4J',),
                searchpath=(), url=_stanford_url,
                verbose=verbose)

        self._stanford_jar = ':'.join(
            [_ for _ in [stanford_segmenter, slf4j] if not _ is None])

        self._sihan_corpora_dict = path_to_sihan_corpora_dict
        self._model = path_to_model
        self._dict = path_to_dict

        self._encoding = encoding
        self.java_options = java_options
        options = {} if options is None else options
        self._options_cmd = ','.join('{0}={1}'.format(key, json.dumps(val)) for key, val in options.items())

    def tokenize(self, s):
        super().tokenize(s)

    def segment_file(self, input_file_path):
        """
        """
        cmd = [
            'edu.stanford.nlp.ie.crf.CRFClassifier',
            '-sighanCorporaDict', self._sihan_corpora_dict,
            '-textFile', input_file_path,
            '-sighanPostProcessing', 'true',
            '-keepAllWhitespaces', 'false',
            '-loadClassifier', self._model,
            '-serDictionary', self._dict
        ]

        stdout = self._execute(cmd)

        return stdout

    def segment(self, tokens):
        return self.segment_sents([tokens])

    def segment_sents(self, sentences):
        """
        """
        encoding = self._encoding
        # Create a temporary input file
        _input_fh, self._input_file_path = tempfile.mkstemp(text=True)

        # Write the actural sentences to the temporary input file
        _input_fh = os.fdopen(_input_fh, 'wb')
        _input = '\n'.join((' '.join(x) for x in sentences))
        if isinstance(_input, compat.text_type) and encoding:
            _input = _input.encode(encoding)
        _input_fh.write(_input)
        _input_fh.close()

        cmd = [
            'edu.stanford.nlp.ie.crf.CRFClassifier',
            '-sighanCorporaDict', self._sihan_corpora_dict,
            '-textFile', self._input_file_path,
            '-sighanPostProcessing', 'true',
            '-keepAllWhitespaces', 'false',
            '-loadClassifier', self._model,
            '-serDictionary', self._dict
        ]

        stdout = self._execute(cmd)

        # Delete the temporary file
        os.unlink(self._input_file_path)

        return stdout

    def _execute(self, cmd, verbose=False):
        encoding = self._encoding
        cmd.extend(['-inputEncoding', encoding])
        _options_cmd = self._options_cmd
        if _options_cmd:
            cmd.extend(['-options', self._options_cmd])

        default_options = ' '.join(_java_options)

        # Configure java.
        config_java(options=self.java_options, verbose=verbose)

        stdout, _stderr = java(
            cmd,classpath=self._stanford_jar, stdout=PIPE, stderr=PIPE)
        stdout = stdout.decode(encoding)

        # Return java configurations to their default values.
        config_java(options=default_options, verbose=False)

        return stdout

def setup_module(module):
    from nose import SkipTest

    try:
        StanfordSegmenter()
    except LookupError:
        raise SkipTest('doctests from nltk.tokenize.stanford_segmenter are skipped because the stanford segmenter jar doesn\'t exist')
<commit_msg>Comment.<commit_after>#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Natural Language Toolkit: Interface to the Stanford Chinese Segmenter
#
# Copyright (C) 2001-2016 NLTK Project
# Author: 52nlp <52nlpcn@gmail.com>
#         Casper Lehmann-Strøm <casperlehmann@gmail.com>
#
# URL: <http://nltk.org/>
# For license information, see LICENSE.TXT

from __future__ import unicode_literals, print_function

import tempfile
import os
import json
from subprocess import PIPE

from nltk import compat
from nltk.internals import find_jar, config_java, java, _java_options

from nltk.tokenize.api import TokenizerI

_stanford_url = 'http://nlp.stanford.edu/software'

class StanfordSegmenter(TokenizerI):
    r"""
    Interface to the Stanford Segmenter
    >>> from nltk.tokenize.stanford_segmenter import StanfordSegmenter
    >>> segmenter = StanfordSegmenter(
    ...     path_to_jar="stanford-segmenter-3.6.0.jar",
    ...     path_to_slf4j = "slf4j-api.jar"
    ...     path_to_sihan_corpora_dict="./data",
    ...     path_to_model="./data/pku.gz",
    ...     path_to_dict="./data/dict-chris6.ser.gz")
    >>> sentence = u"这是斯坦福中文分词器测试"
    >>> segmenter.segment(sentence)
    >>> u'\u8fd9 \u662f \u65af\u5766\u798f \u4e2d\u6587 \u5206\u8bcd\u5668 \u6d4b\u8bd5\n'
    >>> segmenter.segment_file("test.simp.utf8")
    >>> u'\u9762\u5bf9 \u65b0 \u4e16\u7eaa \uff0c \u4e16\u754c \u5404\u56fd ...
    """

    _JAR = 'stanford-segmenter.jar'
    _SLF4J = 'slf4j-api.jar'

    def __init__(self, path_to_jar=None, path_to_slf4j=None,
            path_to_sihan_corpora_dict=None,
            path_to_model=None, path_to_dict=None,
            encoding='UTF-8', options=None,
            verbose=False, java_options='-mx2g'):
        stanford_segmenter = find_jar(
                self._JAR, path_to_jar,
                env_vars=('STANFORD_SEGMENTER',),
                searchpath=(), url=_stanford_url,
                verbose=verbose)
        slf4j = find_jar(
                self._SLF4J, path_to_slf4j,
                env_vars=('SLF4J',),
                searchpath=(), url=_stanford_url,
                verbose=verbose)

        # This is passed to java as the -cp option, the segmenter needs slf4j.
        self._stanford_jar = ':'.join(
            [_ for _ in [stanford_segmenter, slf4j] if not _ is None])

        self._sihan_corpora_dict = path_to_sihan_corpora_dict
        self._model = path_to_model
        self._dict = path_to_dict

        self._encoding = encoding
        self.java_options = java_options
        options = {} if options is None else options
        self._options_cmd = ','.join('{0}={1}'.format(key, json.dumps(val)) for key, val in options.items())

    def tokenize(self, s):
        super().tokenize(s)

    def segment_file(self, input_file_path):
        """
        """
        cmd = [
            'edu.stanford.nlp.ie.crf.CRFClassifier',
            '-sighanCorporaDict', self._sihan_corpora_dict,
            '-textFile', input_file_path,
            '-sighanPostProcessing', 'true',
            '-keepAllWhitespaces', 'false',
            '-loadClassifier', self._model,
            '-serDictionary', self._dict
        ]

        stdout = self._execute(cmd)

        return stdout

    def segment(self, tokens):
        return self.segment_sents([tokens])

    def segment_sents(self, sentences):
        """
        """
        encoding = self._encoding
        # Create a temporary input file
        _input_fh, self._input_file_path = tempfile.mkstemp(text=True)

        # Write the actural sentences to the temporary input file
        _input_fh = os.fdopen(_input_fh, 'wb')
        _input = '\n'.join((' '.join(x) for x in sentences))
        if isinstance(_input, compat.text_type) and encoding:
            _input = _input.encode(encoding)
        _input_fh.write(_input)
        _input_fh.close()

        cmd = [
            'edu.stanford.nlp.ie.crf.CRFClassifier',
            '-sighanCorporaDict', self._sihan_corpora_dict,
            '-textFile', self._input_file_path,
            '-sighanPostProcessing', 'true',
            '-keepAllWhitespaces', 'false',
            '-loadClassifier', self._model,
            '-serDictionary', self._dict
        ]

        stdout = self._execute(cmd)

        # Delete the temporary file
        os.unlink(self._input_file_path)

        return stdout

    def _execute(self, cmd, verbose=False):
        encoding = self._encoding
        cmd.extend(['-inputEncoding', encoding])
        _options_cmd = self._options_cmd
        if _options_cmd:
            cmd.extend(['-options', self._options_cmd])

        default_options = ' '.join(_java_options)

        # Configure java.
        config_java(options=self.java_options, verbose=verbose)

        stdout, _stderr = java(
            cmd,classpath=self._stanford_jar, stdout=PIPE, stderr=PIPE)
        stdout = stdout.decode(encoding)

        # Return java configurations to their default values.
        config_java(options=default_options, verbose=False)

        return stdout

def setup_module(module):
    from nose import SkipTest

    try:
        StanfordSegmenter()
    except LookupError:
        raise SkipTest('doctests from nltk.tokenize.stanford_segmenter are skipped because the stanford segmenter jar doesn\'t exist')
<|endoftext|>


<commit_before>from peewee import *
from playhouse.csv_loader import *
from Models import *
import os

def backupDb():
    directory = "backup"
    if not os.path.exists(directory):
        os.mkdir(directory)

    # Export User
    out = "username,password,firstName,lastName,email,karma,strikes\n"
    for user in User.select().order_by(User.key):
        line = user.username + "," \
                + user.password + "," \
                + user.firstName + "," \
                + user.lastName + "," \
                + user.email + "," \
                + str(user.karma) + "," \
                + str(user.strikes) + "\n"
        out += line
    fh = open("backup/User.csv", "w")
    fh.write(out)
    fh.close()

    # Export Admins
    out = "user\n"
    for admin in Admin.select().order_by(Admin.key):
        line = str(admin.user.key) + "\n"
        out += line
    fh = open("backup/Admin.csv", "w")
    fh.write(out)
    fh.close()

    # Export Regexes
    out = "pattern\n"
    for regex in Regex.select().order_by(Regex.key):
        line = regex.pattern + "\n"
        out += line
    fh = open("backup/Regex.csv", "w")
    fh.write(out)
    fh.close()

    # Export SpamReport
    out = "offender,reporter,youtubeKey,title,uploader,description,date\n"
    for spamReport in SpamReport.select().order_by(SpamReport.key):
        line = str(pamReport.offender.key) + "," \
             + str(spamReport.reporter.key) + "," \
             + spamReport.youtubeKey + "," \
             + spamReport.title + "," \
             + spamReport.uploader + "," \
             + spamReport.description + "," \
             + str(spamReport.date) + "\n"
        out += line
    fh = open("backup/SpamReport.csv", "w")
    fh.write(out)
    fh.close()

def importDb():
    directory = "import"

    if os.path.exists(directory):
        tables = [
            User,
            AccessToken,
            Admin,
            Regex,
            Request,
            CurrentRequest,
            Upvote,
            Downvote,
            Spam,
            SpamReport,
            Command
            ]
        db.drop_tables(tables, safe=True)
        db.create_tables(tables, safe=True)

        load_csv(User, "import/User.csv")
        load_csv(Admin, "import/Admin.csv")
        load_csv(Regex, "import/Regex.csv")
        load_csv(SpamReport, "import/SpamReport.csv")
        return "Imported Models"
    else:
        return "No import dir"

<commit_msg>Add favorites to the list of tables to drop when backing up<commit_after>from peewee import *
from playhouse.csv_loader import *
from Models import *
import os

def backupDb():
    directory = "backup"
    if not os.path.exists(directory):
        os.mkdir(directory)

    # Export User
    out = "username,password,firstName,lastName,email,karma,strikes\n"
    for user in User.select().order_by(User.key):
        line = user.username + "," \
                + user.password + "," \
                + user.firstName + "," \
                + user.lastName + "," \
                + user.email + "," \
                + str(user.karma) + "," \
                + str(user.strikes) + "\n"
        out += line
    fh = open("backup/User.csv", "w")
    fh.write(out)
    fh.close()

    # Export Admins
    out = "user\n"
    for admin in Admin.select().order_by(Admin.key):
        line = str(admin.user.key) + "\n"
        out += line
    fh = open("backup/Admin.csv", "w")
    fh.write(out)
    fh.close()

    # Export Regexes
    out = "pattern\n"
    for regex in Regex.select().order_by(Regex.key):
        line = regex.pattern + "\n"
        out += line
    fh = open("backup/Regex.csv", "w")
    fh.write(out)
    fh.close()

    # Export SpamReport
    out = "offender,reporter,youtubeKey,title,uploader,description,date\n"
    for spamReport in SpamReport.select().order_by(SpamReport.key):
        line = str(pamReport.offender.key) + "," \
             + str(spamReport.reporter.key) + "," \
             + spamReport.youtubeKey + "," \
             + spamReport.title + "," \
             + spamReport.uploader + "," \
             + spamReport.description + "," \
             + str(spamReport.date) + "\n"
        out += line
    fh = open("backup/SpamReport.csv", "w")
    fh.write(out)
    fh.close()

def importDb():
    directory = "import"

    if os.path.exists(directory):
        tables = [
            User,
            AccessToken,
            Admin,
            Regex,
            Request,
            CurrentRequest,
            Upvote,
            Downvote,
            Spam,
            SpamReport,
            Command,
	    Favorite
            ]
        db.drop_tables(tables, safe=True)
        db.create_tables(tables, safe=True)

        load_csv(User, "import/User.csv")
        load_csv(Admin, "import/Admin.csv")
        load_csv(Regex, "import/Regex.csv")
        load_csv(SpamReport, "import/SpamReport.csv")
        return "Imported Models"
    else:
        return "No import dir"

<|endoftext|>


<commit_before>import logging
import re
import inspect

import discogs_client as discogs

logger = logging.getLogger(__name__)

class memoized_property(object):

    def __init__(self, fget, doc=None):
        self.fget = fget
        self.__doc__ = doc or fget.__doc__
        self.__name__ = fget.__name__

    def __get__(self, obj, cls):
        if obj is None:
            return self
        obj.__dict__[self.__name__] = result = self.fget(obj)
        return result

class TrackContainer(object):

    pass

class DiscogsAlbum(object):
    """ Wraps the discogs-client-api script, abstracting the minimal set of
        artist data required to tag an album/release

        >>> from discogstagger.discogsalbum import DiscogsAlbum
        >>> release = DiscogsAlbum(40522) # fetch discogs release id 40522
        >>> print "%s - %s (%s / %s)" % (release.artist, release.title, release.catno,
        >>> release.label)

        Blunted Dummies - House For All (12DEF006 / Definitive Recordings)

        >>> for song in release.tracks: print "[ %.2d ] %s - %s" % (song.position,
        >>> song.artist, song.title)

        [ 01 ] Blunted Dummies - House For All (Original Mix)
        [ 02 ] Blunted Dummies - House For All (House 4 All Robots Mix)
        [ 03 ] Blunted Dummies - House For All (Eddie Richard's Mix)
        [ 04 ] Blunted Dummies - House For All (J. Acquaviva's Mix)
        [ 05 ] Blunted Dummies - House For All (Ruby Fruit Jungle Mix) """
# remove all not needed parameters (these should be not handled in here)
    def __init__(self, releaseid, split_artists, split_genres_and_styles):

        discogs.user_agent = "discogstagger +http://github.com/jesseward"
        self.release = discogs.Release(releaseid)

        self.split_artists = split_artists
        self.split_genres_and_styles = split_genres_and_styles

        self.discs = {}
        logger.info("Fetching %s - %s (%s)" % (self.artist, self.title,
                    releaseid))

    def __str__(self):

        return "<%s - %s>" % (self.artist, self.title)

    @property
    def album_info(self):
        """ Dumps the release data to a formatted text string. Formatted for
            .nfo file  """

        logger.debug("Writing nfo file")
        div = "_ _______________________________________________ _ _\n"
        r = div
        r += "  Name : %s - %s\n" % (self.artist, self.title)
        r += " Label : %s\n" % (self.label)
        r += " Genre : %s\n" % (self.genre)
        r += " Catno : %s\n" % (self.catno)
        r += "  Year : %s\n" % (self.year)
        r += "   URL : %s\n" % (self.url)

        if self.master_id:
            r += "Master : http://www.discogs.com/master/%s\n" % self.master_id

        r += div
        for song in self.tracks:
            r += "%.2d. %s - %s\n" % (song.position, song.artist, song.title)
        return r

    @property
    def releaseid(self):
        """ retuns the discogs release id """

        return self.release._id

    @property
    def url(self):
        """ returns the discogs url of this release """

        return "http://www.discogs.com/release/%s" % self.release._id

    @property
    def catno(self):
        """ Returns the release catalog number """

        return self.release.data["labels"][0]["catno"]

    @property
    def label(self):
        """ Returns the release Label name """

        return self.clean_name(self.release.data["labels"][0]["name"])

    @property
    def images(self):
        """ return a single list of images for the given album """

        try:
            return [x["uri"] for x in self.release.data["images"]]
        except KeyError:
            pass

    @property
    def title(self):
        """ return the album release name from discogs API """

        return self.release.title

    @property
    def year(self):
        """ returns the album release year obtained from API 2.0 """

        good_year = re.compile("\d\d\d\d")
        try:
            return good_year.match(str(self.release.data["year"])).group(0)
        except IndexError:
            return "1900"

    @property
    def master_id(self):
        """ returns the master release id """

        try:
            return self.release.data["master_id"]
        except KeyError:
            return None

    @property
    def genre(self):
        """ obtain the album genre """

        return self.release.data["genres"][0]

    @property
    def genres(self):
        """ obtain the album genre """

        rel_genres = self.split_genres_and_styles.join(self.release.data["genres"])
        return rel_genres

    @property
    def style(self):
        """ obtain the album styles """

        return self.release.data["styles"][0]

    @property
    def styles(self):
        """ obtain the album styles in one field """

        rel_styles = self.split_genres_and_styles.join(self.release.data["styles"])
        return rel_styles

    def _gen_artist(self, artist_data):
        """ yields a list of normalized release artists name properties """

        for x in artist_data:
            yield x.name

    @property
    def country(self):
        """ Obtain the country - a not so easy field, because it could mean
            the label country, the recording country, or.... """

        return self.release.data["country"]

    @property
    def artists(self):
        """ obtain the album artists """
        return self._gen_artist(self.release.artists)

    @property
    def artist(self):
        """ obtain the album artist """

        rel_artist = self.split_artists.join(self._gen_artist(self.release.artists))
        return self.clean_name(rel_artist)

    @property
    def sort_artist(self):
        """ obtain the album artist """

        return self.release.artists[0].name

    @property
    def note(self):
        """ obtain the note """
        value = False
        if "notes" in self.release.data:
            value = self.release.data["notes"]
        return value

    def disc_and_track_no(self, position):
        """ obtain the disc and tracknumber from given position """

        # some variance in how discogs releases spanning multiple discs
        # or formats are kept, add regexs here as failures are encountered
        NUMBERING_SCHEMES = (
            "^CD(?P<discnumber>\d+)-(?P<tracknumber>\d+)$", # CD01-12
            "^(?P<discnumber>\d+)-(?P<tracknumber>\d+)$",   # 1-02
            "^(?P<discnumber>\d+).(?P<tracknumber>\d+)$",   # 1.05
        )

        for scheme in NUMBERING_SCHEMES:
            re_match = re.search(scheme, position)

            if re_match:
                logging.debug("Found a disc and track number")
                return {'tracknumber': re_match.group("tracknumber"),
                        'discnumber': re_match.group("discnumber")}

        logging.error("Unable to match multi-disc track/position")
        return False

    @property
    def disctotal(self):
        return int(self.release.data["formats"][0]["qty"])

    def tracktotal_on_disc(self, discnumber):
        logger.debug("discs: %s" % self.discs)
        return self.discs[discnumber]

    @property
    def is_compilation(self):
        if self.release.data["artists"][0]["name"] == "Various":
            return True

        for format in self.release.data["formats"]:
            if "descriptions" in format:
                for description in format["descriptions"]:
                    if description == "compilation":
                        return True

        return False

    @memoized_property
    def tracks(self):
        """ provides the tracklist of the given release id """

        logger.error(inspect.stack()[1])
        track_list = []
        discsubtitle = None

        for i, t in enumerate((x for x in self.release.tracklist
                              if x["type"] == "Track")):

            # this is pretty much the same as the artist
            # stuff in the album, try to refactor it
            try:
                sort_artist = self.clean_name(t["artists"][0].name)
                artist = self.split_artists.join(self._gen_artist(t["artists"]))
                artist = self.clean_name(artist)
            except IndexError:
                artist = self.artist
                sort_artist = self.sort_artist

            track = TrackContainer()

            # on multiple discs there do appears a subtitle as the first "track"
            # on the cd in discogs, this seems to be wrong, but we would like to
            # handle it anyway
            if t["title"] and not t["position"] and not t["duration"]:
                discsubtitle = t["title"]
                continue

            track.position = i + 1

            if self.disctotal > 1:
                pos = self.disc_and_track_no(t["position"])
                track.tracknumber = int(pos["tracknumber"])
                track.discnumber = int(pos["discnumber"])
            else:
                track.tracknumber = int(t["position"])
                track.discnumber = 1
            self.discs[int(track.discnumber)] = int(track.tracknumber)

            if discsubtitle:
                track.discsubtitle = discsubtitle

            track.sortartist = sort_artist
            track.artist = artist

            track.title = t["title"]
            track_list.append(track)

        return track_list

    @staticmethod
    def clean_name(clean_target):
        """ Cleans up the format of the artist or label name provided by
            Discogs.
            Examples:
                'Goldie (12)' becomes 'Goldie'
                  or
                'Aphex Twin, The' becomes 'The Aphex Twin'
            Accepts a string to clean, returns a cleansed version """

        groups = {
            "(.*),\sThe$": "The",
        }

        # remove discogs duplicate handling eg : John (1)
        clean_target = re.sub("\s\(\d+\)", "", clean_target)

        for regex in groups:
            if re.search(r"%s" % regex, clean_target):
                clean_target = "%s %s" % (groups[regex],
                                          re.search("%s" % regex,
                                          clean_target).group(1))
        return clean_target
<commit_msg>bugfix: ensure we yield the name property from artist object cleanup: remove temp debugging data.<commit_after>import logging
import re

import discogs_client as discogs

logger = logging.getLogger(__name__)

class memoized_property(object):

    def __init__(self, fget, doc=None):
        self.fget = fget
        self.__doc__ = doc or fget.__doc__
        self.__name__ = fget.__name__

    def __get__(self, obj, cls):
        if obj is None:
            return self
        obj.__dict__[self.__name__] = result = self.fget(obj)
        return result

class TrackContainer(object):

    pass

class DiscogsAlbum(object):
    """ Wraps the discogs-client-api script, abstracting the minimal set of
        artist data required to tag an album/release

        >>> from discogstagger.discogsalbum import DiscogsAlbum
        >>> release = DiscogsAlbum(40522) # fetch discogs release id 40522
        >>> print "%s - %s (%s / %s)" % (release.artist, release.title, release.catno,
        >>> release.label)

        Blunted Dummies - House For All (12DEF006 / Definitive Recordings)

        >>> for song in release.tracks: print "[ %.2d ] %s - %s" % (song.position,
        >>> song.artist, song.title)

        [ 01 ] Blunted Dummies - House For All (Original Mix)
        [ 02 ] Blunted Dummies - House For All (House 4 All Robots Mix)
        [ 03 ] Blunted Dummies - House For All (Eddie Richard's Mix)
        [ 04 ] Blunted Dummies - House For All (J. Acquaviva's Mix)
        [ 05 ] Blunted Dummies - House For All (Ruby Fruit Jungle Mix) """
# remove all not needed parameters (these should be not handled in here)
    def __init__(self, releaseid, split_artists, split_genres_and_styles):

        discogs.user_agent = "discogstagger +http://github.com/jesseward"
        self.release = discogs.Release(releaseid)

        self.split_artists = split_artists
        self.split_genres_and_styles = split_genres_and_styles

        self.discs = {}
        logger.info("Fetching %s - %s (%s)" % (self.artist, self.title,
                    releaseid))

    def __str__(self):

        return "<%s - %s>" % (self.artist, self.title)

    @property
    def album_info(self):
        """ Dumps the release data to a formatted text string. Formatted for
            .nfo file  """

        logger.debug("Writing nfo file")
        div = "_ _______________________________________________ _ _\n"
        r = div
        r += "  Name : %s - %s\n" % (self.artist, self.title)
        r += " Label : %s\n" % (self.label)
        r += " Genre : %s\n" % (self.genre)
        r += " Catno : %s\n" % (self.catno)
        r += "  Year : %s\n" % (self.year)
        r += "   URL : %s\n" % (self.url)

        if self.master_id:
            r += "Master : http://www.discogs.com/master/%s\n" % self.master_id

        r += div
        for song in self.tracks:
            r += "%.2d. %s - %s\n" % (song.position, song.artist, song.title)
        return r

    @property
    def releaseid(self):
        """ retuns the discogs release id """

        return self.release._id

    @property
    def url(self):
        """ returns the discogs url of this release """

        return "http://www.discogs.com/release/%s" % self.release._id

    @property
    def catno(self):
        """ Returns the release catalog number """

        return self.release.data["labels"][0]["catno"]

    @property
    def label(self):
        """ Returns the release Label name """

        return self.clean_name(self.release.data["labels"][0]["name"])

    @property
    def images(self):
        """ return a single list of images for the given album """

        try:
            return [x["uri"] for x in self.release.data["images"]]
        except KeyError:
            pass

    @property
    def title(self):
        """ return the album release name from discogs API """

        return self.release.title

    @property
    def year(self):
        """ returns the album release year obtained from API 2.0 """

        good_year = re.compile("\d\d\d\d")
        try:
            return good_year.match(str(self.release.data["year"])).group(0)
        except IndexError:
            return "1900"

    @property
    def master_id(self):
        """ returns the master release id """

        try:
            return self.release.data["master_id"]
        except KeyError:
            return None

    @property
    def genre(self):
        """ obtain the album genre """

        return self.release.data["genres"][0]

    @property
    def genres(self):
        """ obtain the album genre """

        rel_genres = self.split_genres_and_styles.join(self.release.data["genres"])
        return rel_genres

    @property
    def style(self):
        """ obtain the album styles """

        return self.release.data["styles"][0]

    @property
    def styles(self):
        """ obtain the album styles in one field """

        rel_styles = self.split_genres_and_styles.join(self.release.data["styles"])
        return rel_styles

    def _gen_artist(self, artist_data):
        """ yields a list of normalized release artists name properties """

        for x in artist_data:
            # bugfix to avoid the following scenario, or ensure we're yielding
            # and artist object.
            # AttributeError: 'unicode' object has no attribute 'name'
            # [<Artist "A.D.N.Y*">, u'Presents', <Artist "Leiva">]
            try:
                yield x.name
            except AttributeError:
                pass

    @property
    def country(self):
        """ Obtain the country - a not so easy field, because it could mean
            the label country, the recording country, or.... """

        return self.release.data["country"]

    @property
    def artists(self):
        """ obtain the album artists """
        return self._gen_artist(self.release.artists)

    @property
    def artist(self):
        """ obtain the album artist """

        rel_artist = self.split_artists.join(self._gen_artist(self.release.artists))
        return self.clean_name(rel_artist)

    @property
    def sort_artist(self):
        """ obtain the album artist """

        return self.release.artists[0].name

    @property
    def note(self):
        """ obtain the note """
        value = False
        if "notes" in self.release.data:
            value = self.release.data["notes"]
        return value

    def disc_and_track_no(self, position):
        """ obtain the disc and tracknumber from given position """

        # some variance in how discogs releases spanning multiple discs
        # or formats are kept, add regexs here as failures are encountered
        NUMBERING_SCHEMES = (
            "^CD(?P<discnumber>\d+)-(?P<tracknumber>\d+)$", # CD01-12
            "^(?P<discnumber>\d+)-(?P<tracknumber>\d+)$",   # 1-02
            "^(?P<discnumber>\d+).(?P<tracknumber>\d+)$",   # 1.05
        )

        for scheme in NUMBERING_SCHEMES:
            re_match = re.search(scheme, position)

            if re_match:
                logging.debug("Found a disc and track number")
                return {'tracknumber': re_match.group("tracknumber"),
                        'discnumber': re_match.group("discnumber")}

        logging.error("Unable to match multi-disc track/position")
        return False

    @property
    def disctotal(self):
        return int(self.release.data["formats"][0]["qty"])

    def tracktotal_on_disc(self, discnumber):
        logger.debug("discs: %s" % self.discs)
        return self.discs[discnumber]

    @property
    def is_compilation(self):
        if self.release.data["artists"][0]["name"] == "Various":
            return True

        for format in self.release.data["formats"]:
            if "descriptions" in format:
                for description in format["descriptions"]:
                    if description == "compilation":
                        return True

        return False

    @memoized_property
    def tracks(self):
        """ provides the tracklist of the given release id """

        track_list = []
        discsubtitle = None

        for i, t in enumerate((x for x in self.release.tracklist
                              if x["type"] == "Track")):

            # this is pretty much the same as the artist
            # stuff in the album, try to refactor it
            try:
                sort_artist = self.clean_name(t["artists"][0].name)
                artist = self.split_artists.join(self._gen_artist(t["artists"]))
                artist = self.clean_name(artist)
            except IndexError:
                artist = self.artist
                sort_artist = self.sort_artist

            track = TrackContainer()

            # on multiple discs there do appears a subtitle as the first "track"
            # on the cd in discogs, this seems to be wrong, but we would like to
            # handle it anyway
            if t["title"] and not t["position"] and not t["duration"]:
                discsubtitle = t["title"]
                continue

            track.position = i + 1

            if self.disctotal > 1:
                pos = self.disc_and_track_no(t["position"])
                track.tracknumber = int(pos["tracknumber"])
                track.discnumber = int(pos["discnumber"])
            else:
                track.tracknumber = int(t["position"])
                track.discnumber = 1
            self.discs[int(track.discnumber)] = int(track.tracknumber)

            if discsubtitle:
                track.discsubtitle = discsubtitle

            track.sortartist = sort_artist
            track.artist = artist

            track.title = t["title"]
            track_list.append(track)

        return track_list

    @staticmethod
    def clean_name(clean_target):
        """ Cleans up the format of the artist or label name provided by
            Discogs.
            Examples:
                'Goldie (12)' becomes 'Goldie'
                  or
                'Aphex Twin, The' becomes 'The Aphex Twin'
            Accepts a string to clean, returns a cleansed version """

        groups = {
            "(.*),\sThe$": "The",
        }

        # remove discogs duplicate handling eg : John (1)
        clean_target = re.sub("\s\(\d+\)", "", clean_target)

        for regex in groups:
            if re.search(r"%s" % regex, clean_target):
                clean_target = "%s %s" % (groups[regex],
                                          re.search("%s" % regex,
                                          clean_target).group(1))
        return clean_target
<|endoftext|>


<commit_before>import asyncio
import datetime
import aiohttp
import websockets
import discord
import inspect
import logging
import sys
import traceback

from discord.backoff import ExponentialBackoff

log = logging.getLogger(__name__)

class Loop:
    """A background task helper that abstracts the loop and reconnection logic for you.

    The main interface to create this is through :func:`loop`.
    """
    def __init__(self, coro, seconds, hours, minutes, count, reconnect, loop):
        self.coro = coro
        self.reconnect = reconnect
        self.loop = loop or asyncio.get_event_loop()
        self.count = count
        self._current_loop = 0
        self._task = None
        self._injected = None
        self._valid_exception = (
            OSError,
            discord.HTTPException,
            discord.GatewayNotFound,
            discord.ConnectionClosed,
            aiohttp.ClientError,
            asyncio.TimeoutError,
            websockets.InvalidHandshake,
            websockets.WebSocketProtocolError,
        )

        self._before_loop = None
        self._after_loop = None
        self._is_being_cancelled = False
        self._has_failed = False
        self._stop_next_iteration = False

        if self.count is not None and self.count <= 0:
            raise ValueError('count must be greater than 0 or None.')

        self.change_interval(seconds=seconds, minutes=minutes, hours=hours)
        self._last_iteration = None
        self._next_iteration = None

        if not inspect.iscoroutinefunction(self.coro):
            raise TypeError('Expected coroutine function, not {0.__name__!r}.'.format(type(self.coro)))

    async def _call_loop_function(self, name, *args, **kwargs):
        coro = getattr(self, '_' + name)
        if coro is None:
            return

        if self._injected is not None:
            await coro(self._injected, *args, **kwargs)
        else:
            await coro(*args, **kwargs)

    async def _loop(self, *args, **kwargs):
        backoff = ExponentialBackoff()
        await self._call_loop_function('before_loop')
        sleep_until = discord.utils.sleep_until
        self._next_iteration = datetime.datetime.now(datetime.timezone.utc)
        try:
            await asyncio.sleep(0) # allows canceling in before_loop
            while True:
                self._last_iteration = self._next_iteration
                self._next_iteration = self._get_next_sleep_time()
                try:
                    await self.coro(*args, **kwargs)
                    now = datetime.datetime.now(datetime.timezone.utc)
                    if now > self._next_iteration:
                        self._next_iteration = now
                except self._valid_exception as exc:
                    if not self.reconnect:
                        raise
                    await asyncio.sleep(backoff.delay())
                else:
                    if self._stop_next_iteration:
                        return
                    self._current_loop += 1
                    if self._current_loop == self.count:
                        break

                    await sleep_until(self._next_iteration)
        except asyncio.CancelledError:
            self._is_being_cancelled = True
            raise
        except Exception as exc:
            self._has_failed = True
            await self._call_loop_function('error', exc)
            raise exc
        finally:
            await self._call_loop_function('after_loop')
            self._is_being_cancelled = False
            self._current_loop = 0
            self._stop_next_iteration = False
            self._has_failed = False

    def __get__(self, obj, objtype):
        if obj is None:
            return self

        copy = Loop(self.coro, seconds=self.seconds, hours=self.hours, minutes=self.minutes,
                               count=self.count, reconnect=self.reconnect, loop=self.loop)
        copy._injected = obj
        copy._before_loop = self._before_loop
        copy._after_loop = self._after_loop
        copy._error = self._error
        setattr(obj, self.coro.__name__, copy)
        return copy

    @property
    def current_loop(self):
        """:class:`int`: The current iteration of the loop."""
        return self._current_loop

    @property
    def next_iteration(self):
        """Optional[:class:`datetime.datetime`]: When the next iteration of the loop will occur.

        .. versionadded:: 1.3
        """
        if self._task is None and self._sleep:
            return None
        elif self._task and self._task.done() or self._stop_next_iteration:
            return None
        return self._next_iteration

    def start(self, *args, **kwargs):
        r"""Starts the internal task in the event loop.

        Parameters
        ------------
        \*args
            The arguments to use.
        \*\*kwargs
            The keyword arguments to use.

        Raises
        --------
        RuntimeError
            A task has already been launched and is running.

        Returns
        ---------
        :class:`asyncio.Task`
            The task that has been created.
        """

        if self._task is not None and not self._task.done():
            raise RuntimeError('Task is already launched and is not completed.')

        if self._injected is not None:
            args = (self._injected, *args)

        self._task = self.loop.create_task(self._loop(*args, **kwargs))
        return self._task

    def stop(self):
        r"""Gracefully stops the task from running.

        Unlike :meth:`cancel`\, this allows the task to finish its
        current iteration before gracefully exiting.

        .. note::

            If the internal function raises an error that can be
            handled before finishing then it will retry until
            it succeeds.

            If this is undesirable, either remove the error handling
            before stopping via :meth:`clear_exception_types` or
            use :meth:`cancel` instead.

        .. versionadded:: 1.2
        """
        if self._task and not self._task.done():
            self._stop_next_iteration = True

    def _can_be_cancelled(self):
        return not self._is_being_cancelled and self._task and not self._task.done()

    def cancel(self):
        """Cancels the internal task, if it is running."""
        if self._can_be_cancelled():
            self._task.cancel()

    def restart(self, *args, **kwargs):
        r"""A convenience method to restart the internal task.

        .. note::

            Due to the way this function works, the task is not
            returned like :meth:`start`.

        Parameters
        ------------
        \*args
            The arguments to to use.
        \*\*kwargs
            The keyword arguments to use.
        """

        def restart_when_over(fut, *, args=args, kwargs=kwargs):
            self._task.remove_done_callback(restart_when_over)
            self.start(*args, **kwargs)

        if self._can_be_cancelled():
            self._task.add_done_callback(restart_when_over)
            self._task.cancel()

    def add_exception_type(self, *exceptions):
        r"""Adds exception types to be handled during the reconnect logic.

        By default the exception types handled are those handled by
        :meth:`discord.Client.connect`\, which includes a lot of internet disconnection
        errors.

        This function is useful if you're interacting with a 3rd party library that
        raises its own set of exceptions.

        Parameters
        ------------
        \*exceptions: Type[:class:`BaseException`]
            An argument list of exception classes to handle.

        Raises
        --------
        TypeError
            An exception passed is either not a class or not inherited from :class:`BaseException`.
        """

        for exc in exceptions:
            if not inspect.isclass(exc):
                raise TypeError('{0!r} must be a class.'.format(exc))
            if not issubclass(exc, BaseException):
                raise TypeError('{0!r} must inherit from BaseException.'.format(exc))

        self._valid_exception = (*self._valid_exception, *exceptions)

    def clear_exception_types(self):
        """Removes all exception types that are handled.

        .. note::

            This operation obviously cannot be undone!
        """
        self._valid_exception = tuple()

    def remove_exception_type(self, *exceptions):
        r"""Removes exception types from being handled during the reconnect logic.

        Parameters
        ------------
        \*exceptions: Type[:class:`BaseException`]
            An argument list of exception classes to handle.

        Returns
        ---------
        :class:`bool`
            Whether all exceptions were successfully removed.
        """
        old_length = len(self._valid_exception)
        self._valid_exception = tuple(x for x in self._valid_exception if x not in exceptions)
        return len(self._valid_exception) == old_length - len(exceptions)

    def get_task(self):
        """Optional[:class:`asyncio.Task`]: Fetches the internal task or ``None`` if there isn't one running."""
        return self._task

    def is_being_cancelled(self):
        """Whether the task is being cancelled."""
        return self._is_being_cancelled

    def failed(self):
        """:class:`bool`: Whether the internal task has failed.

        .. versionadded:: 1.2
        """
        return self._has_failed

    def is_running(self):
        """:class:`bool`: Check if the task is currently running.

        .. versionadded:: 1.4
        """
        return not bool(self._task.done()) if self._task else False

    async def _error(self, exception):
        print('Unhandled exception in internal background task {0.__name__!r}.'.format(self.coro), file=sys.stderr)
        traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)

    def before_loop(self, coro):
        """A decorator that registers a coroutine to be called before the loop starts running.

        This is useful if you want to wait for some bot state before the loop starts,
        such as :meth:`discord.Client.wait_until_ready`.

        The coroutine must take no arguments (except ``self`` in a class context).

        Parameters
        ------------
        coro: :ref:`coroutine <coroutine>`
            The coroutine to register before the loop runs.

        Raises
        -------
        TypeError
            The function was not a coroutine.
        """

        if not inspect.iscoroutinefunction(coro):
            raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))

        self._before_loop = coro
        return coro

    def after_loop(self, coro):
        """A decorator that register a coroutine to be called after the loop finished running.

        The coroutine must take no arguments (except ``self`` in a class context).

        .. note::

            This coroutine is called even during cancellation. If it is desirable
            to tell apart whether something was cancelled or not, check to see
            whether :meth:`is_being_cancelled` is ``True`` or not.

        Parameters
        ------------
        coro: :ref:`coroutine <coroutine>`
            The coroutine to register after the loop finishes.

        Raises
        -------
        TypeError
            The function was not a coroutine.
        """

        if not inspect.iscoroutinefunction(coro):
            raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))

        self._after_loop = coro
        return coro

    def error(self, coro):
        """A decorator that registers a coroutine to be called if the task encounters an unhandled exception.

        The coroutine must take only one argument the exception raised (except ``self`` in a class context).

        By default this prints to :data:`sys.stderr` however it could be
        overridden to have a different implementation.

        .. versionadded:: 1.4

        Parameters
        ------------
        coro: :ref:`coroutine <coroutine>`
            The coroutine to register in the event of an unhandled exception.

        Raises
        -------
        TypeError
            The function was not a coroutine.
        """
        if not inspect.iscoroutinefunction(coro):
            raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))

        self._error = coro
        return coro

    def _get_next_sleep_time(self):
        return self._last_iteration + datetime.timedelta(seconds=self._sleep)

    def change_interval(self, *, seconds=0, minutes=0, hours=0):
        """Changes the interval for the sleep time.

        .. note::

            This only applies on the next loop iteration. If it is desirable for the change of interval
            to be applied right away, cancel the task with :meth:`cancel`.

        .. versionadded:: 1.2

        Parameters
        ------------
        seconds: :class:`float`
            The number of seconds between every iteration.
        minutes: :class:`float`
            The number of minutes between every iteration.
        hours: :class:`float`
            The number of hours between every iteration.

        Raises
        -------
        ValueError
            An invalid value was given.
        """

        sleep = seconds + (minutes * 60.0) + (hours * 3600.0)
        if sleep < 0:
            raise ValueError('Total number of seconds cannot be less than zero.')

        self._sleep = sleep
        self.seconds = seconds
        self.hours = hours
        self.minutes = minutes

def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None):
    """A decorator that schedules a task in the background for you with
    optional reconnect logic. The decorator returns a :class:`Loop`.

    Parameters
    ------------
    seconds: :class:`float`
        The number of seconds between every iteration.
    minutes: :class:`float`
        The number of minutes between every iteration.
    hours: :class:`float`
        The number of hours between every iteration.
    count: Optional[:class:`int`]
        The number of loops to do, ``None`` if it should be an
        infinite loop.
    reconnect: :class:`bool`
        Whether to handle errors and restart the task
        using an exponential back-off algorithm similar to the
        one used in :meth:`discord.Client.connect`.
    loop: :class:`asyncio.AbstractEventLoop`
        The loop to use to register the task, if not given
        defaults to :func:`asyncio.get_event_loop`.

    Raises
    --------
    ValueError
        An invalid value was given.
    TypeError
        The function was not a coroutine.
    """
    def decorator(func):
        kwargs = {
            'seconds': seconds,
            'minutes': minutes,
            'hours': hours,
            'count': count,
            'reconnect': reconnect,
            'loop': loop
        }
        return Loop(func, **kwargs)
    return decorator
<commit_msg>[tasks] Fix issue with default error handler in class context<commit_after>import asyncio
import datetime
import aiohttp
import websockets
import discord
import inspect
import logging
import sys
import traceback

from discord.backoff import ExponentialBackoff

log = logging.getLogger(__name__)

class Loop:
    """A background task helper that abstracts the loop and reconnection logic for you.

    The main interface to create this is through :func:`loop`.
    """
    def __init__(self, coro, seconds, hours, minutes, count, reconnect, loop):
        self.coro = coro
        self.reconnect = reconnect
        self.loop = loop or asyncio.get_event_loop()
        self.count = count
        self._current_loop = 0
        self._task = None
        self._injected = None
        self._valid_exception = (
            OSError,
            discord.HTTPException,
            discord.GatewayNotFound,
            discord.ConnectionClosed,
            aiohttp.ClientError,
            asyncio.TimeoutError,
            websockets.InvalidHandshake,
            websockets.WebSocketProtocolError,
        )

        self._before_loop = None
        self._after_loop = None
        self._is_being_cancelled = False
        self._has_failed = False
        self._stop_next_iteration = False

        if self.count is not None and self.count <= 0:
            raise ValueError('count must be greater than 0 or None.')

        self.change_interval(seconds=seconds, minutes=minutes, hours=hours)
        self._last_iteration = None
        self._next_iteration = None

        if not inspect.iscoroutinefunction(self.coro):
            raise TypeError('Expected coroutine function, not {0.__name__!r}.'.format(type(self.coro)))

    async def _call_loop_function(self, name, *args, **kwargs):
        coro = getattr(self, '_' + name)
        if coro is None:
            return

        if self._injected is not None:
            await coro(self._injected, *args, **kwargs)
        else:
            await coro(*args, **kwargs)

    async def _loop(self, *args, **kwargs):
        backoff = ExponentialBackoff()
        await self._call_loop_function('before_loop')
        sleep_until = discord.utils.sleep_until
        self._next_iteration = datetime.datetime.now(datetime.timezone.utc)
        try:
            await asyncio.sleep(0) # allows canceling in before_loop
            while True:
                self._last_iteration = self._next_iteration
                self._next_iteration = self._get_next_sleep_time()
                try:
                    await self.coro(*args, **kwargs)
                    now = datetime.datetime.now(datetime.timezone.utc)
                    if now > self._next_iteration:
                        self._next_iteration = now
                except self._valid_exception as exc:
                    if not self.reconnect:
                        raise
                    await asyncio.sleep(backoff.delay())
                else:
                    if self._stop_next_iteration:
                        return
                    self._current_loop += 1
                    if self._current_loop == self.count:
                        break

                    await sleep_until(self._next_iteration)
        except asyncio.CancelledError:
            self._is_being_cancelled = True
            raise
        except Exception as exc:
            self._has_failed = True
            await self._call_loop_function('error', exc)
            raise exc
        finally:
            await self._call_loop_function('after_loop')
            self._is_being_cancelled = False
            self._current_loop = 0
            self._stop_next_iteration = False
            self._has_failed = False

    def __get__(self, obj, objtype):
        if obj is None:
            return self

        copy = Loop(self.coro, seconds=self.seconds, hours=self.hours, minutes=self.minutes,
                               count=self.count, reconnect=self.reconnect, loop=self.loop)
        copy._injected = obj
        copy._before_loop = self._before_loop
        copy._after_loop = self._after_loop
        copy._error = self._error
        setattr(obj, self.coro.__name__, copy)
        return copy

    @property
    def current_loop(self):
        """:class:`int`: The current iteration of the loop."""
        return self._current_loop

    @property
    def next_iteration(self):
        """Optional[:class:`datetime.datetime`]: When the next iteration of the loop will occur.

        .. versionadded:: 1.3
        """
        if self._task is None and self._sleep:
            return None
        elif self._task and self._task.done() or self._stop_next_iteration:
            return None
        return self._next_iteration

    def start(self, *args, **kwargs):
        r"""Starts the internal task in the event loop.

        Parameters
        ------------
        \*args
            The arguments to use.
        \*\*kwargs
            The keyword arguments to use.

        Raises
        --------
        RuntimeError
            A task has already been launched and is running.

        Returns
        ---------
        :class:`asyncio.Task`
            The task that has been created.
        """

        if self._task is not None and not self._task.done():
            raise RuntimeError('Task is already launched and is not completed.')

        if self._injected is not None:
            args = (self._injected, *args)

        self._task = self.loop.create_task(self._loop(*args, **kwargs))
        return self._task

    def stop(self):
        r"""Gracefully stops the task from running.

        Unlike :meth:`cancel`\, this allows the task to finish its
        current iteration before gracefully exiting.

        .. note::

            If the internal function raises an error that can be
            handled before finishing then it will retry until
            it succeeds.

            If this is undesirable, either remove the error handling
            before stopping via :meth:`clear_exception_types` or
            use :meth:`cancel` instead.

        .. versionadded:: 1.2
        """
        if self._task and not self._task.done():
            self._stop_next_iteration = True

    def _can_be_cancelled(self):
        return not self._is_being_cancelled and self._task and not self._task.done()

    def cancel(self):
        """Cancels the internal task, if it is running."""
        if self._can_be_cancelled():
            self._task.cancel()

    def restart(self, *args, **kwargs):
        r"""A convenience method to restart the internal task.

        .. note::

            Due to the way this function works, the task is not
            returned like :meth:`start`.

        Parameters
        ------------
        \*args
            The arguments to to use.
        \*\*kwargs
            The keyword arguments to use.
        """

        def restart_when_over(fut, *, args=args, kwargs=kwargs):
            self._task.remove_done_callback(restart_when_over)
            self.start(*args, **kwargs)

        if self._can_be_cancelled():
            self._task.add_done_callback(restart_when_over)
            self._task.cancel()

    def add_exception_type(self, *exceptions):
        r"""Adds exception types to be handled during the reconnect logic.

        By default the exception types handled are those handled by
        :meth:`discord.Client.connect`\, which includes a lot of internet disconnection
        errors.

        This function is useful if you're interacting with a 3rd party library that
        raises its own set of exceptions.

        Parameters
        ------------
        \*exceptions: Type[:class:`BaseException`]
            An argument list of exception classes to handle.

        Raises
        --------
        TypeError
            An exception passed is either not a class or not inherited from :class:`BaseException`.
        """

        for exc in exceptions:
            if not inspect.isclass(exc):
                raise TypeError('{0!r} must be a class.'.format(exc))
            if not issubclass(exc, BaseException):
                raise TypeError('{0!r} must inherit from BaseException.'.format(exc))

        self._valid_exception = (*self._valid_exception, *exceptions)

    def clear_exception_types(self):
        """Removes all exception types that are handled.

        .. note::

            This operation obviously cannot be undone!
        """
        self._valid_exception = tuple()

    def remove_exception_type(self, *exceptions):
        r"""Removes exception types from being handled during the reconnect logic.

        Parameters
        ------------
        \*exceptions: Type[:class:`BaseException`]
            An argument list of exception classes to handle.

        Returns
        ---------
        :class:`bool`
            Whether all exceptions were successfully removed.
        """
        old_length = len(self._valid_exception)
        self._valid_exception = tuple(x for x in self._valid_exception if x not in exceptions)
        return len(self._valid_exception) == old_length - len(exceptions)

    def get_task(self):
        """Optional[:class:`asyncio.Task`]: Fetches the internal task or ``None`` if there isn't one running."""
        return self._task

    def is_being_cancelled(self):
        """Whether the task is being cancelled."""
        return self._is_being_cancelled

    def failed(self):
        """:class:`bool`: Whether the internal task has failed.

        .. versionadded:: 1.2
        """
        return self._has_failed

    def is_running(self):
        """:class:`bool`: Check if the task is currently running.

        .. versionadded:: 1.4
        """
        return not bool(self._task.done()) if self._task else False

    async def _error(self, *args):
        exception = args[-1]
        print('Unhandled exception in internal background task {0.__name__!r}.'.format(self.coro), file=sys.stderr)
        traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)

    def before_loop(self, coro):
        """A decorator that registers a coroutine to be called before the loop starts running.

        This is useful if you want to wait for some bot state before the loop starts,
        such as :meth:`discord.Client.wait_until_ready`.

        The coroutine must take no arguments (except ``self`` in a class context).

        Parameters
        ------------
        coro: :ref:`coroutine <coroutine>`
            The coroutine to register before the loop runs.

        Raises
        -------
        TypeError
            The function was not a coroutine.
        """

        if not inspect.iscoroutinefunction(coro):
            raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))

        self._before_loop = coro
        return coro

    def after_loop(self, coro):
        """A decorator that register a coroutine to be called after the loop finished running.

        The coroutine must take no arguments (except ``self`` in a class context).

        .. note::

            This coroutine is called even during cancellation. If it is desirable
            to tell apart whether something was cancelled or not, check to see
            whether :meth:`is_being_cancelled` is ``True`` or not.

        Parameters
        ------------
        coro: :ref:`coroutine <coroutine>`
            The coroutine to register after the loop finishes.

        Raises
        -------
        TypeError
            The function was not a coroutine.
        """

        if not inspect.iscoroutinefunction(coro):
            raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))

        self._after_loop = coro
        return coro

    def error(self, coro):
        """A decorator that registers a coroutine to be called if the task encounters an unhandled exception.

        The coroutine must take only one argument the exception raised (except ``self`` in a class context).

        By default this prints to :data:`sys.stderr` however it could be
        overridden to have a different implementation.

        .. versionadded:: 1.4

        Parameters
        ------------
        coro: :ref:`coroutine <coroutine>`
            The coroutine to register in the event of an unhandled exception.

        Raises
        -------
        TypeError
            The function was not a coroutine.
        """
        if not inspect.iscoroutinefunction(coro):
            raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))

        self._error = coro
        return coro

    def _get_next_sleep_time(self):
        return self._last_iteration + datetime.timedelta(seconds=self._sleep)

    def change_interval(self, *, seconds=0, minutes=0, hours=0):
        """Changes the interval for the sleep time.

        .. note::

            This only applies on the next loop iteration. If it is desirable for the change of interval
            to be applied right away, cancel the task with :meth:`cancel`.

        .. versionadded:: 1.2

        Parameters
        ------------
        seconds: :class:`float`
            The number of seconds between every iteration.
        minutes: :class:`float`
            The number of minutes between every iteration.
        hours: :class:`float`
            The number of hours between every iteration.

        Raises
        -------
        ValueError
            An invalid value was given.
        """

        sleep = seconds + (minutes * 60.0) + (hours * 3600.0)
        if sleep < 0:
            raise ValueError('Total number of seconds cannot be less than zero.')

        self._sleep = sleep
        self.seconds = seconds
        self.hours = hours
        self.minutes = minutes

def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None):
    """A decorator that schedules a task in the background for you with
    optional reconnect logic. The decorator returns a :class:`Loop`.

    Parameters
    ------------
    seconds: :class:`float`
        The number of seconds between every iteration.
    minutes: :class:`float`
        The number of minutes between every iteration.
    hours: :class:`float`
        The number of hours between every iteration.
    count: Optional[:class:`int`]
        The number of loops to do, ``None`` if it should be an
        infinite loop.
    reconnect: :class:`bool`
        Whether to handle errors and restart the task
        using an exponential back-off algorithm similar to the
        one used in :meth:`discord.Client.connect`.
    loop: :class:`asyncio.AbstractEventLoop`
        The loop to use to register the task, if not given
        defaults to :func:`asyncio.get_event_loop`.

    Raises
    --------
    ValueError
        An invalid value was given.
    TypeError
        The function was not a coroutine.
    """
    def decorator(func):
        kwargs = {
            'seconds': seconds,
            'minutes': minutes,
            'hours': hours,
            'count': count,
            'reconnect': reconnect,
            'loop': loop
        }
        return Loop(func, **kwargs)
    return decorator
<|endoftext|>


<commit_before># Copyright 2014-2016 The ODL development group
#
# This file is part of ODL.
#
# ODL is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ODL is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ODL.  If not, see <http://www.gnu.org/licenses/>.

# Imports for common Python 2/3 codebase
from __future__ import print_function, division, absolute_import
from future import standard_library
standard_library.install_aliases()

import numpy as np
from odl.discr import ResizingOperator
from odl.trafos import FourierTransform, PYFFTW_AVAILABLE

__all__ = ('fbp_op',)

def fbp_op(ray_trafo, padding=True):
    """Create filtered back-projection from a `RayTransform`.

    The filtered back-projection is an approximate inverse to the ray
    transform.

    Parameters
    ----------
    ray_trafo : `RayTransform`
        The ray transform (forward operator) whose approximate inverse should
        be computed. Its geometry has to be any of the following

        `Parallel2DGeometry` : Exact reconstruction

        `Parallel3dAxisGeometry` : Exact reconstruction

        `FanFlatGeometry` : Approximate reconstruction, correct in limit of fan
        angle = 0.

        `CircularConeFlatGeometry` : Approximate reconstruction, correct in
        limit of fan angle = 0 and cone angle = 0.

        `HelicalConeFlatGeometry` : Very approximate.

        Other geometries: Not supported

    padding : bool, optional
        If the data space should be zero padded. Without padding, the data may
        be corrupted due to the circular convolution used. Using padding makes
        the algorithm slower.

    Returns
    -------
    fbp : `Operator`
        Approximate inverse operator of ``ray_trafo``.
    """
    impl = 'pyfftw' if PYFFTW_AVAILABLE else 'numpy'
    alen = ray_trafo.geometry.motion_params.length

    if ray_trafo.domain.ndim == 2:
        # Define ramp filter
        def fft_filter(x):
            return np.sqrt(x[1]**2) / (2 * alen)

        # Define (padded) fourier transform
        if padding:
            # Define padding operator
            ran_shp = (ray_trafo.range.shape[0],
                       ray_trafo.range.shape[1] * 2 - 1)
            resizing = ResizingOperator(ray_trafo.range, ran_shp=ran_shp)

            fourier = FourierTransform(resizing.range, axes=1, impl=impl)
            fourier = fourier * resizing
        else:
            fourier = FourierTransform(ray_trafo.range, axes=1, impl=impl)

    elif ray_trafo.domain.ndim == 3:
        # Find the direction that the filter should be taken in
        du = ray_trafo.geometry.det_init_axes[0]
        dv = ray_trafo.geometry.det_init_axes[1]
        axis = ray_trafo.geometry.axis
        det_normal = np.cross(du, dv)
        rot_dir = np.cross(axis, det_normal)
        c = np.array([np.vdot(rot_dir, du), np.vdot(rot_dir, dv)])
        cnorm = np.linalg.norm(c)
        assert cnorm != 0
        c /= cnorm

        used_axes = c != 0
        if used_axes[0] and not used_axes[1]:
            axes = [1]
        elif not used_axes[0] and used_axes[1]:
            axes = [2]
        else:
            axes = [1, 2]

        # Add scaling for cone-beam case
        if hasattr(ray_trafo.geometry, 'src_radius'):
            scale = (ray_trafo.geometry.src_radius /
                     (ray_trafo.geometry.src_radius +
                      ray_trafo.geometry.det_radius))
        else:
            scale = 1.0

        # Define ramp filter
        def fft_filter(x):
            return np.abs(c[0] * x[1] + c[1] * x[2]) * (scale / (2 * alen))

        # Define (padded) fourier transform
        if padding:
            # Define padding operator
            if used_axes[0]:
                padded_shape_u = ray_trafo.range.shape[1] * 2 - 1
            else:
                padded_shape_u = ray_trafo.range.shape[1]

            if used_axes[1]:
                padded_shape_v = ray_trafo.range.shape[2] * 2 - 1
            else:
                padded_shape_v = ray_trafo.range.shape[2]

            ran_shp = (ray_trafo.range.shape[0],
                       padded_shape_u,
                       padded_shape_v)
            resizing = ResizingOperator(ray_trafo.range, ran_shp=ran_shp)

            fourier = FourierTransform(resizing.range, axes=axes, impl=impl)
            fourier = fourier * resizing
        else:
            fourier = FourierTransform(ray_trafo.range, axes=axes, impl=impl)
    else:
        raise NotImplementedError('FBP only implemented in 2d and 3d')

    # Create ramp in the detector direction
    ramp_function = fourier.range.element(fft_filter)

    # Create ramp filter via the convolution formula with fourier transforms
    ramp_filter = fourier.inverse * ramp_function * fourier

    # Create filtered backprojection by composing the backprojection
    # (adjoint) with the ramp filter.
    return ray_trafo.adjoint * ramp_filter

if __name__ == '__main__':
    # pylint: disable=wrong-import-position
    from odl.util.testutils import run_doctests
    run_doctests()
<commit_msg>ENH: Add fbp_filter option to fbp_op<commit_after># Copyright 2014-2016 The ODL development group
#
# This file is part of ODL.
#
# ODL is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ODL is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ODL.  If not, see <http://www.gnu.org/licenses/>.

# Imports for common Python 2/3 codebase
from __future__ import print_function, division, absolute_import
from future import standard_library
standard_library.install_aliases()

import numpy as np
from odl.discr import ResizingOperator
from odl.trafos import FourierTransform, PYFFTW_AVAILABLE

__all__ = ('fbp_op',)

def fbp_filter(norm_freq, filter_type, filter_cutoff):
    """Create a smoothing filter for FBP.

    Parameters
    ----------
    norm_freq : `array-like`
        Normalized frequencies.
    filter_type : {'Ram-Lak', 'Shepp-Logan', 'Cosine', 'Hamming', 'Hann'}
        The type of filter to be used.
    filter_cutoff : float
        Relative cutoff frequency for the filter.

    Returns
    -------
    smoothing_filter : `numpy.ndarray`

    References
    ----------
    http://oftankonyv.reak.bme.hu/tiki-index.php?page=Reconstruction
    """
    if not (0 < filter_cutoff <= 1):
        raise ValueError('`filter_cutoff` ({}) not in the interval (0, 1]'
                         ''.format(filter_cutoff))

    indicator = (norm_freq <= filter_cutoff)

    if filter_type == 'Ram-Lak':
        filt = indicator
    elif filter_type == 'Shepp-Logan':
        filt = indicator * np.sinc(norm_freq / (2 * filter_cutoff))
    elif filter_type == 'Cosine':
        filt = indicator * np.cos(norm_freq * np.pi / (2 * filter_cutoff))
    elif filter_type == 'Hamming':
        filt = indicator * (0.54 +
                            0.46 * np.cos(norm_freq * np.pi / (filter_cutoff)))
    elif filter_type == 'Hann':
        filt = indicator * np.cos(norm_freq * np.pi / (2 * filter_cutoff)) ** 2
    else:
        raise ValueError('unknown `filter_type` ({})'
                         ''.format(filter_type))

    return filt

def fbp_op(ray_trafo, padding=True, filter_type='Ram-Lak', filter_cutoff=1.0):
    """Create filtered back-projection from a `RayTransform`.

    The filtered back-projection is an approximate inverse to the ray
    transform.

    Parameters
    ----------
    ray_trafo : `RayTransform`
        The ray transform (forward operator) whose approximate inverse should
        be computed. Its geometry has to be any of the following

        `Parallel2DGeometry` : Exact reconstruction

        `Parallel3dAxisGeometry` : Exact reconstruction

        `FanFlatGeometry` : Approximate reconstruction, correct in limit of fan
        angle = 0.

        `CircularConeFlatGeometry` : Approximate reconstruction, correct in
        limit of fan angle = 0 and cone angle = 0.

        `HelicalConeFlatGeometry` : Very approximate.

        Other geometries: Not supported

    padding : bool, optional
        If the data space should be zero padded. Without padding, the data may
        be corrupted due to the circular convolution used. Using padding makes
        the algorithm slower.
    filter_type : {'Ram-Lak', 'Shepp-Logan', 'Cosine', 'Hamming', 'Hann'}
        The type of filter to be used.
    filter_cutoff : float
        Relative cutoff frequency for the filter.

    Returns
    -------
    fbp : `Operator`
        Approximate inverse operator of ``ray_trafo``.
    """
    impl = 'pyfftw' if PYFFTW_AVAILABLE else 'numpy'
    alen = ray_trafo.geometry.motion_params.length

    if ray_trafo.domain.ndim == 2:
        # Define ramp filter
        def fft_filter(x):
            norm_freq = np.abs(x[1]) / np.max(np.abs(x[1]))
            filt = smoothing_filter(norm_freq, filter_type, filter_cutoff)
            ramp = np.sqrt(x[1]**2)
            scaling = 1 / (2 * alen)
            return filt * ramp * scaling

        # Define (padded) fourier transform
        if padding:
            # Define padding operator
            ran_shp = (ray_trafo.range.shape[0],
                       ray_trafo.range.shape[1] * 2 - 1)
            resizing = ResizingOperator(ray_trafo.range, ran_shp=ran_shp)

            fourier = FourierTransform(resizing.range, axes=1, impl=impl)
            fourier = fourier * resizing
        else:
            fourier = FourierTransform(ray_trafo.range, axes=1, impl=impl)

    elif ray_trafo.domain.ndim == 3:
        # Find the direction that the filter should be taken in
        du = ray_trafo.geometry.det_init_axes[0]
        dv = ray_trafo.geometry.det_init_axes[1]
        axis = ray_trafo.geometry.axis
        det_normal = np.cross(du, dv)
        rot_dir = np.cross(axis, det_normal)
        c = np.array([np.vdot(rot_dir, du), np.vdot(rot_dir, dv)])
        cnorm = np.linalg.norm(c)
        assert cnorm != 0
        c /= cnorm

        used_axes = c != 0
        if used_axes[0] and not used_axes[1]:
            axes = [1]
        elif not used_axes[0] and used_axes[1]:
            axes = [2]
        else:
            axes = [1, 2]

        # Add scaling for cone-beam case
        if hasattr(ray_trafo.geometry, 'src_radius'):
            scale = (ray_trafo.geometry.src_radius /
                     (ray_trafo.geometry.src_radius +
                      ray_trafo.geometry.det_radius))
        else:
            scale = 1.0

        # Define ramp filter
        def fft_filter(x):
            return np.abs(c[0] * x[1] + c[1] * x[2]) * (scale / (2 * alen))

        # Define (padded) fourier transform
        if padding:
            # Define padding operator
            if used_axes[0]:
                padded_shape_u = ray_trafo.range.shape[1] * 2 - 1
            else:
                padded_shape_u = ray_trafo.range.shape[1]

            if used_axes[1]:
                padded_shape_v = ray_trafo.range.shape[2] * 2 - 1
            else:
                padded_shape_v = ray_trafo.range.shape[2]

            ran_shp = (ray_trafo.range.shape[0],
                       padded_shape_u,
                       padded_shape_v)
            resizing = ResizingOperator(ray_trafo.range, ran_shp=ran_shp)

            fourier = FourierTransform(resizing.range, axes=axes, impl=impl)
            fourier = fourier * resizing
        else:
            fourier = FourierTransform(ray_trafo.range, axes=axes, impl=impl)
    else:
        raise NotImplementedError('FBP only implemented in 2d and 3d')

    # Create ramp in the detector direction
    ramp_function = fourier.range.element(fft_filter)

    # Create ramp filter via the convolution formula with fourier transforms
    ramp_filter = fourier.inverse * ramp_function * fourier

    # Create filtered backprojection by composing the backprojection
    # (adjoint) with the ramp filter.
    return ray_trafo.adjoint * ramp_filter

if __name__ == '__main__':
    # Display the various filters
    import matplotlib.pyplot as plt
    x = np.linspace(0, 1, 100)
    cutoff = 0.9

    for filter_name in ['Ram-Lak', 'Shepp-Logan', 'Cosine', 'Hamming', 'Hann']:
        plt.plot(x, x * fbp_filter(x, filter_name, cutoff), label=filter_name)

    plt.title('Filters with cutoff = {}'.format(cutoff))
    plt.legend(loc=2)

    # pylint: disable=wrong-import-position
    from odl.util.testutils import run_doctests
    run_doctests()
<|endoftext|>


<commit_before>#!/usr/bin/env python

import argparse
import curses
import os.path
import random
import signal
import sys
import time

PADDING = 5
DEAD_CHAR = '.'
FILL_CHAR = ' '
INTERVAL = 0.05

class Life:

    def __init__(self, seed_file, padding):
        self.dead_char = DEAD_CHAR
        self.padding = padding
        self.parse_seed(seed_file)

    def parse_seed(self, seed_file):
        """ Parse the Game starting layout from the seed file. """
        with open(seed_file, 'r') as seed:
            lines = seed.readlines()

        # Dimensions of grid that get displayed.
        self.disp_rows = len(lines)
        self.disp_cols = len(lines[0].strip())

        # Internal dimensions of grid after accounting for padding.
        self.rows = self.disp_rows + self.padding * 2
        self.cols = self.disp_cols + self.padding * 2

        # Make a new grid where the Game with take place.
        self.grid = self.make_empty_grid()

        # Copy the contents of the file into their proper places in the grid.
        for row in range(self.disp_rows):
            for col in range(self.disp_cols):
                self.grid[row + self.padding][col + self.padding] = lines[row][col]

    def get_neighbours(self, row, col):
        """ Gathers all the live neighbours of the cell. """
        neighbours = []

        # Positions of all neighbouring cells.
        neighbour_indices = [(row - 1, col - 1), (row - 1, col),
                (row - 1, col + 1), (row, col - 1), (row, col + 1),
                (row + 1, col - 1), (row + 1, col), (row + 1, col + 1)]

        # Add live neighbours to the list.
        for (row, col) in neighbour_indices:
            try:
                char = self.grid[row][col]
                if char != self.dead_char:
                    neighbours.append(char)
            except IndexError:
                pass
        return neighbours

    def generate_cell(self, row, col):
        """ Generates a the next state of a cell based on its neighbours. """
        neighbours = self.get_neighbours(row, col)
        num_neighbours = len(neighbours)
        # Live cells continue to survive if they have only two or three
        # neighbours.
        if self.grid[row][col] != self.dead_char:
            if num_neighbours < 2:
                return self.dead_char
            if num_neighbours > 3:
                return self.dead_char
            return self.grid[row][col]

        # If the cell is dead and surrounding be three live cells, it gets
        # 'born'.
        if num_neighbours == 3:
            if neighbours[0] == neighbours[1]  == neighbours[2]:
                return neighbours[0]
            if neighbours[0] == neighbours[1] or neighbours[0] == neighbours[2]:
                return neighbours[0]
            if neighbours[1] == neighbours[2]:
                return neighbours[1]
            return neighbours[random.randint(0, 2)]

        return self.dead_char

    def next_generation(self):
        """ Updates the grid with a new generation of cells. """
        new_grid = self.make_empty_grid()
        for row in range(self.rows):
            for col in range(self.cols):
                new_grid[row][col] = self.generate_cell(row, col)
        self.grid = new_grid

    def display(self, stdscr):
        """ Print the grid to the screen. """
        hor_border = '{c:{b}<{w}}{c}'.format(c='+', b='-', w=self.disp_cols+1)

        stdscr.addstr(hor_border + '\n')
        for row in range(self.padding, self.rows - self.padding):
            row_str = '|'
            for col in range(self.padding, self.cols - self.padding):
                char = self.grid[row][col]
                if char == self.dead_char:
                    row_str = row_str + FILL_CHAR
                else:
                    row_str = row_str + char
            stdscr.addstr(row_str + '|\n')
        stdscr.addstr(hor_border + '\n')

    def make_empty_grid(self):
        """ Makes a grid for the Game initialized with the dead character. """
        return [[self.dead_char for col in range(self.cols)]
                for row in range(self.rows)]

def handle_keys(stdscr, pause):
    """ Handles key presses that occur during the Game loop. """
    c = stdscr.getch()
    if c == ord('q'):
        sys.exit(0)
    elif c == ord('p') or pause:
        pause = True
        stdscr.nodelay(False)
        while True:
            c = stdscr.getch()
            if c == ord('q'):
                sys.exit(0)
            elif c == ord('p'):
                pause = False
                break
            elif c == ord('s'):
                break
        stdscr.nodelay(True)
    return pause

def main(stdscr):
    """ Setup and main Game loop. """

    # Parse command line arguments.
    parser = argparse.ArgumentParser()
    parser.add_argument('seed', help='Seed file for the Game.')
    parser.add_argument('-p', '--padding', help='Amount of padding outside of '
                        'visible area.', dest='padding', type=int,
                        default=PADDING)
    parser.add_argument('-t', '--time-interval', help='Time in seconds between '
                        'each generation.', dest='interval', type=float,
                        default=INTERVAL)
    args = parser.parse_args()

    # Argument error checking.
    if not os.path.exists(args.seed):
        raise IOError('Seed file {} does not exist!'.format(args.seed))
    if args.padding < 0:
        raise ValueError('--padding must not be negative.')
    if args.interval < 0:
        raise ValueError('--time-interval must not be negative.')

    stdscr.nodelay(True)

    # Create a new instance of Game of Life.
    life = Life(args.seed, args.padding)

    # Display title and instructions.
    stdscr.addstr("Multicell\nConway's Game of Life with a twist.")
    stdscr.move(life.disp_rows + 4, 0)
    stdscr.addstr("Press 'q' to quit, 'p' to pause/unpause, and 's' to step "
                  "when paused.\n")

    pause = False

    # Game loop.
    while True:
        stdscr.move(2, 0)
        life.display(stdscr)
        stdscr.refresh()
        stdscr.move(life.disp_rows + 5, 0)
        life.next_generation()
        pause = handle_keys(stdscr, pause)
        time.sleep(args.interval)

if __name__ == '__main__':
    curses.wrapper(main)
<commit_msg>Added license text and nicer help text to life.<commit_after>#!/usr/bin/env python
#
# Copyright (c) 2015 Adam Heins
#
# This file is part of the Multicell project, which is distributed under the MIT
# license. For the full terms, see the included LICENSE file.
#

import argparse
import curses
import os.path
import random
import signal
import sys
import time

PADDING = 5
DEAD_CHAR = '.'
FILL_CHAR = ' '
INTERVAL = 0.05

class Life:

    def __init__(self, seed_file, padding):
        self.dead_char = DEAD_CHAR
        self.padding = padding
        self.parse_seed(seed_file)

    def parse_seed(self, seed_file):
        """ Parse the Game starting layout from the seed file. """
        with open(seed_file, 'r') as seed:
            lines = seed.readlines()

        # Dimensions of grid that get displayed.
        self.disp_rows = len(lines)
        self.disp_cols = len(lines[0].strip())

        # Internal dimensions of grid after accounting for padding.
        self.rows = self.disp_rows + self.padding * 2
        self.cols = self.disp_cols + self.padding * 2

        # Make a new grid where the Game with take place.
        self.grid = self.make_empty_grid()

        # Copy the contents of the file into their proper places in the grid.
        for row in range(self.disp_rows):
            for col in range(self.disp_cols):
                self.grid[row + self.padding][col + self.padding] = lines[row][col]

    def get_neighbours(self, row, col):
        """ Gathers all the live neighbours of the cell. """
        neighbours = []

        # Positions of all neighbouring cells.
        neighbour_indices = [(row - 1, col - 1), (row - 1, col),
                (row - 1, col + 1), (row, col - 1), (row, col + 1),
                (row + 1, col - 1), (row + 1, col), (row + 1, col + 1)]

        # Add live neighbours to the list.
        for (row, col) in neighbour_indices:
            try:
                char = self.grid[row][col]
                if char != self.dead_char:
                    neighbours.append(char)
            except IndexError:
                pass
        return neighbours

    def generate_cell(self, row, col):
        """ Generates a the next state of a cell based on its neighbours. """
        neighbours = self.get_neighbours(row, col)
        num_neighbours = len(neighbours)
        # Live cells continue to survive if they have only two or three
        # neighbours.
        if self.grid[row][col] != self.dead_char:
            if num_neighbours < 2:
                return self.dead_char
            if num_neighbours > 3:
                return self.dead_char
            return self.grid[row][col]

        # If the cell is dead and surrounding be three live cells, it gets
        # 'born'.
        if num_neighbours == 3:
            if neighbours[0] == neighbours[1]  == neighbours[2]:
                return neighbours[0]
            if neighbours[0] == neighbours[1] or neighbours[0] == neighbours[2]:
                return neighbours[0]
            if neighbours[1] == neighbours[2]:
                return neighbours[1]
            return neighbours[random.randint(0, 2)]

        return self.dead_char

    def next_generation(self):
        """ Updates the grid with a new generation of cells. """
        new_grid = self.make_empty_grid()
        for row in range(self.rows):
            for col in range(self.cols):
                new_grid[row][col] = self.generate_cell(row, col)
        self.grid = new_grid

    def display(self, stdscr):
        """ Print the grid to the screen. """
        hor_border = '{c:{b}<{w}}{c}'.format(c='+', b='-', w=self.disp_cols+1)

        stdscr.addstr(hor_border + '\n')
        for row in range(self.padding, self.rows - self.padding):
            row_str = '|'
            for col in range(self.padding, self.cols - self.padding):
                char = self.grid[row][col]
                if char == self.dead_char:
                    row_str = row_str + FILL_CHAR
                else:
                    row_str = row_str + char
            stdscr.addstr(row_str + '|\n')
        stdscr.addstr(hor_border + '\n')

    def make_empty_grid(self):
        """ Makes a grid for the Game initialized with the dead character. """
        return [[self.dead_char for col in range(self.cols)]
                for row in range(self.rows)]

def handle_keys(stdscr, pause):
    """ Handles key presses that occur during the Game loop. """
    c = stdscr.getch()
    if c == ord('q'):
        sys.exit(0)
    elif c == ord('p') or pause:
        pause = True
        stdscr.nodelay(False)
        while True:
            c = stdscr.getch()
            if c == ord('q'):
                sys.exit(0)
            elif c == ord('p'):
                pause = False
                break
            elif c == ord('s'):
                break
        stdscr.nodelay(True)
    return pause

def game(stdscr):
    """ Main game loop. """
    stdscr.nodelay(True)

    # Create a new instance of Game of Life.
    life = Life(args.seed, args.padding)

    # Display title and instructions.
    stdscr.addstr("Multicell\nConway's Game of Life with a twist.")
    stdscr.move(life.disp_rows + 4, 0)
    stdscr.addstr("Press 'q' to quit, 'p' to pause/unpause, and 's' to step "
                  "when paused.\n")

    pause = False

    # Game loop.
    while True:
        stdscr.move(2, 0)
        life.display(stdscr)
        stdscr.refresh()
        stdscr.move(life.disp_rows + 5, 0)
        life.next_generation()
        pause = handle_keys(stdscr, pause)
        time.sleep(args.interval)

def main():
    """ Setup. """

    # Parse command line arguments.
    parser = argparse.ArgumentParser()
    parser.add_argument('seed', help='Seed file for the Game.')
    parser.add_argument('-p', '--padding', help='Amount of padding outside of '
                        'visible area.', dest='padding', type=int,
                        default=PADDING, metavar='padding')
    parser.add_argument('-t', '--time-interval', help='Time in seconds between '
                        'each generation.', dest='interval', type=float,
                        default=INTERVAL, metavar='time-interval')
    args = parser.parse_args()

    # Argument error checking.
    if not os.path.exists(args.seed):
        raise IOError('Seed file {} does not exist!'.format(args.seed))
    if args.padding < 0:
        raise ValueError('--padding must not be negative.')
    if args.interval < 0:
        raise ValueError('--time-interval must not be negative.')

    curses.wrapper(game)

if __name__ == '__main__':
    main()
<|endoftext|>