<commit_before>from __future__ import absolute_import, division, print_function

import os
import re
import requests
import logging
from subprocess import Popen, PIPE

from .utils import conf_to_dict
from .env import CondaCreator
from .compatibility import FileNotFoundError, urlparse
from .exceptions import HDFSConfigException

logger = logging.getLogger(__name__)

JAR_FILE = "knit-1.0-SNAPSHOT.jar"
JAVA_APP = "io.continuum.knit.Client"

class Knit(object):
    """
    Connection to HDFS/YARN

    Parameters
    ----------
    nn: str
        Namenode hostname/ip
    nn: str
        Namenode hostname/ip
    nn_port: int
        Namenode Port (default: 9000)
    rm: str
        Resource Manager hostname
    rm_port: int
        Resource Manager port (default: 8088)
    autodetect: bool
        Autodetect NN/RM IP/Ports

    Examples
    --------

    >>> k = Knit()
    >>> app_id = k.start('sleep 100', num_containers=5, memory=1024)
    """
    def __init__(self, nn="localhost", nn_port=9000,
                 rm="localhost", rm_port=8088, autodetect=False):

        if autodetect:
             self.nn,  self.nn_port = self._yarn_conf(autodetect)
             self.rm,  self.rm_port = self._hdfs_conf(autodetect)

        self.nn = nn
        self.nn_port = str(nn_port)

        self.rm = rm
        self.rm_port = str(rm_port)

        self.java_lib_dir = os.path.join(os.path.dirname(__file__), "java_libs")
        self.KNIT_HOME = os.environ.get('KNIT_HOME') or self.java_lib_dir

        # must set KNIT_HOME ENV for YARN App
        os.environ['KNIT_HOME'] = self.KNIT_HOME
        self._hdfs_conf()
        self._yarn_conf()

    @property
    def JAR_FILE_PATH(self):
        return os.path.join(self.KNIT_HOME, JAR_FILE)

    def _yarn_conf(self, autodetect=False):
        """
        Load YARN config from default locations.
        Parameters
        ----------
        autodetect: bool

        Returns
        -------
            tuple (ip, port)

        """
        confd = os.environ.get('HADOOP_CONF_DIR', os.environ.get('HADOOP_INSTALL',
                               '') + '/hadoop/conf')
        afile = 'yarn-site.xml'
        conf = {}

        try:
            conf.update(conf_to_dict(os.sep.join([confd, afile])))
        except FileNotFoundError:
            pass
        if 'yarn.resourcemanager.webapp.address' in conf:
            url = conf['yarn.resourcemanager.webapp.address']
            u = urlparse(url)

            # handle host:port with no :// preabmle
            if u.path == url:
                conf['host'], conf['port'] = url.split(':')
            else:
                conf['host'] = u.hostname
                conf['port'] = u.port
        else:
            conf['host'] = "localhost"
            conf['port'] = "8088"

        if autodetect:
            return (conf['host'], conf['port'])

        if self.rm != conf['host']:
            msg = "Possible Resource Manager hostname mismatch.  Detected {}".format(conf['host'])
            raise HDFSConfigException(msg)
        if self.rm_port != conf['port']:
            msg = "Possible Resource Manager port mismatch.  Detected {}".format(conf['port'])
            raise HDFSConfigException(msg)

        return conf

    def _hdfs_conf(self, autodetect=False):
        """"
        Parameters
        ----------
        autodetect: bool

        Returns
        -------
            tuple (ip, port)

        """

        confd = os.environ.get('HADOOP_CONF_DIR', os.environ.get('HADOOP_INSTALL',
                               '') + '/hadoop/conf')
        files = 'core-site.xml', 'hdfs-site.xml'
        conf = {}
        for afile in files:
            try:
                conf.update(conf_to_dict(os.sep.join([confd, afile])))
            except FileNotFoundError:
                pass
        if 'fs.defaultFS' in conf:
            u = urlparse(conf['fs.defaultFS'])
            conf['host'] = u.hostname
            conf['port'] = u.port
        else:
            conf['host'] = "localhost"
            conf['port'] = "9000"

        if autodetect:
            return (conf['host'], conf['port'])

        if self.nn != conf['host']:
            msg = "Possible Namenode hostname mismatch.  Detected {}".format(conf['host'])
            raise HDFSConfigException(msg)
        if self.nn_port != str(conf['port']):
            msg = "Possible Namenode port mismatch.  Detected {}".format(conf['port'])
            raise HDFSConfigException(msg)

        return conf

    def start(self, cmd, num_containers=1, virtual_cores=1, memory=128, env=""):
        """
        Method to start a yarn app with a distributed shell

        Parameters
        ----------
        cmd: str
            command to run in each yarn container
        num_containers: int
            number of containers to start (default 1)
        num_containers: int
            Number of containers YARN should request (default: 1)
            * A container should be requested with the number of cores it can saturate, i.e.
            * the average number of threads it expects to have runnable at a time.
        virtual_cores: int
            Number of virtual cores per container (default: 1)
            * A node's capacity should be configured with virtual cores equal to
            * its number of physical cores.
        memory: int
            Memory per container (default: 128)
            * The unit for memory is megabytes.
        env: string
            Full Path to zipped Python environment

        Returns
        -------
        applicationId: str
            A yarn application ID string
        """

        args = ["hadoop", "jar", self.JAR_FILE_PATH, JAVA_APP, "--numContainers", str(num_containers),
                "--command", cmd, "--virtualCores", str(virtual_cores), "--memory", str(memory)]

        if env:
            args = args + ["--pythonEnv", str(env)]

        logger.debug("Running Command: {}".format(' '.join(args)))
        proc = Popen(args, stdout=PIPE, stderr=PIPE)
        out, err = proc.communicate()

        logger.debug(out)
        logger.debug(err)

        # last string in out is applicationId
        # TODO Better JAVA Python communcation: appId, Resources, Yarn, etc.

        appId = out.split()[-1].decode("utf-8")
        appId = re.sub('id', '', appId)
        return appId

    def logs(self, app_id, shell=False):
        """
        Collect logs from each container

        Parameters
        ----------
        app_id: str
             A yarn application ID string
        shell: bool
             Shell out to yarn CLI (default False)

        Returns
        -------
        log: dictionary
            logs from each container
        """

        if shell:

            args = ["yarn", "logs", "-applicationId", app_id]

            proc = Popen(args, stdout=PIPE, stderr=PIPE)
            out, err = proc.communicate()

            logger.debug(out)
            logger.debug(err)
            return str(out)

        host_port = "{}:{}".format(self.rm, self.rm_port)
        url = "http://{}/ws/v1/cluster/apps/{}".format(host_port, app_id)
        logger.debug("Getting Resource Manager Info: {}".format(url))
        r = requests.get(url)
        data = r.json()
        logger.debug(data)

        try:
            amHostHttpAddress = data['app']['amHostHttpAddress']
        except KeyError:
            msg = "Local logs unavailable. State: {} finalStatus: {} Possibly check logs " \
                  "with `yarn logs -applicationId`".format(data['app']['state'],
                                                           data['app']['finalStatus'])
            raise Exception(msg)

        url = "http://{}/ws/v1/node/containers".format(amHostHttpAddress)
        r = requests.get(url)
        data = r.json()['containers']['container']
        logger.debug(data)

        # container_1452274436693_0001_01_000001
        def get_app_id_num(x):
            return "_".join(x.split("_")[1:3])

        app_id_num = get_app_id_num(app_id)
        containers = [d for d in data if get_app_id_num(d['id']) == app_id_num]

        logs = {}
        for c in containers:
            log = {}
            log['nodeId'] = c['nodeId']

            # grab stdout
            url = "{}/stdout/?start=0".format(c['containerLogsLink'])
            logger.debug("Gather stdout/stderr data from {}: {}".format(c['nodeId'], url))
            r = requests.get(url)
            log['stdout'] = r.text

            # grab stderr
            url = "{}/stderr/?start=0".format(c['containerLogsLink'])
            r = requests.get(url)
            log['stderr'] = r.text

            logs[c['id']] = log

        return logs

    def status(self, app_id):
        """ Get status of an application

        Parameters
        ----------
        app_id: str
             A yarn application ID string

        Returns
        -------
        log: dictionary
            status of application
        """
        host_port = "{}:{}".format(self.rm, self.rm_port)
        url = "http://{}/ws/v1/cluster/apps/{}".format(host_port, app_id)
        logger.debug("Getting Application Info: {}".format(url))
        r = requests.get(url)
        data = r.json()

        return data

    def kill(self, application_id):
        """
        Method to kill a yarn application

        Parameters
        ----------
        application_id: str
            YARN applicaiton id

        Returns
        -------
        bool:
            True if successful, False otherwise.
        """

        args = ["yarn", "application", "-kill", application_id]

        proc = Popen(args, stdout=PIPE, stderr=PIPE)
        out, err = proc.communicate()

        logger.debug(out)
        logger.debug(err)

        return any("Killed application" in s for s in [str(out), str(err)])

    @staticmethod
    def create_env(env_name, packages=None, conda_root=None, remove=False):
        """
        Create zipped directory of a conda environment

        Parameters
        ----------
        env_name : str
        packages : list
        conda_root : str, optional
        remove : bool
            remove possible conda environment before creating

        Returns
        -------
        path: str
            path to zipped conda environment

        Examples
        --------

        >>> k = Knit()
        >>> pkg_path = k.create_env(env_name='dev', packages=[python=3, numpy, distributed])
        """

        c = CondaCreator(conda_root=conda_root)
        path = c.create_env(env_name, packages=packages, remove=remove)

        return path

<commit_msg>small fix for autodetecting ip/ports<commit_after>from __future__ import absolute_import, division, print_function

import os
import re
import requests
import logging
from subprocess import Popen, PIPE

from .utils import conf_to_dict
from .env import CondaCreator
from .compatibility import FileNotFoundError, urlparse
from .exceptions import HDFSConfigException

logger = logging.getLogger(__name__)

JAR_FILE = "knit-1.0-SNAPSHOT.jar"
JAVA_APP = "io.continuum.knit.Client"

class Knit(object):
    """
    Connection to HDFS/YARN

    Parameters
    ----------
    nn: str
        Namenode hostname/ip
    nn: str
        Namenode hostname/ip
    nn_port: str
        Namenode Port (default: 9000)
    rm: str
        Resource Manager hostname
    rm_port: str
        Resource Manager port (default: 8088)
    autodetect: bool
        Autodetect NN/RM IP/Ports

    Examples
    --------

    >>> k = Knit()
    >>> app_id = k.start('sleep 100', num_containers=5, memory=1024)
    """
    def __init__(self, nn="localhost", nn_port=9000,
                 rm="localhost", rm_port=8088, autodetect=False):

        self.nn = nn
        self.nn_port = str(nn_port)

        self.rm = rm
        self.rm_port = str(rm_port)

        if autodetect:
            self.nn,  self.nn_port = self._hdfs_conf(autodetect)
            self.rm,  self.rm_port = self._yarn_conf(autodetect)
        else:
            # validates IP/Port is correct
            self._hdfs_conf()
            self._yarn_conf()

        self.java_lib_dir = os.path.join(os.path.dirname(__file__), "java_libs")
        self.KNIT_HOME = os.environ.get('KNIT_HOME') or self.java_lib_dir

        # must set KNIT_HOME ENV for YARN App
        os.environ['KNIT_HOME'] = self.KNIT_HOME

    @property
    def JAR_FILE_PATH(self):
        return os.path.join(self.KNIT_HOME, JAR_FILE)

    def _yarn_conf(self, autodetect=False):
        """
        Load YARN config from default locations.
        Parameters
        ----------
        autodetect: bool

        Returns
        -------
            tuple (ip, port)

        """
        confd = os.environ.get('HADOOP_CONF_DIR', os.environ.get('HADOOP_INSTALL',
                               '') + '/hadoop/conf')
        afile = 'yarn-site.xml'
        conf = {}

        try:
            conf.update(conf_to_dict(os.sep.join([confd, afile])))
        except FileNotFoundError:
            pass
        if 'yarn.resourcemanager.webapp.address' in conf:
            url = conf['yarn.resourcemanager.webapp.address']
            u = urlparse(url)

            # handle host:port with no :// preabmle
            if u.path == url:
                conf['host'], conf['port'] = url.split(':')
            else:
                conf['host'] = u.hostname
                conf['port'] = u.port
        else:
            conf['host'] = "localhost"
            conf['port'] = "8088"

        if autodetect:
            return (conf['host'], conf['port'])

        if self.rm != conf['host']:
            msg = "Possible Resource Manager hostname mismatch.  Detected {}".format(conf['host'])
            raise HDFSConfigException(msg)
        if str(self.rm_port) != str(conf['port']):
            msg = "Possible Resource Manager port mismatch.  Detected {}".format(conf['port'])
            raise HDFSConfigException(msg)

        return conf

    def _hdfs_conf(self, autodetect=False):
        """"
        Parameters
        ----------
        autodetect: bool

        Returns
        -------
            tuple (ip, port)

        """

        confd = os.environ.get('HADOOP_CONF_DIR', os.environ.get('HADOOP_INSTALL',
                               '') + '/hadoop/conf')
        afile = 'core-site.xml'
        conf = {}
        try:
            conf.update(conf_to_dict(os.sep.join([confd, afile])))
        except FileNotFoundError:
            pass

        if 'fs.defaultFS' in conf:
            u = urlparse(conf['fs.defaultFS'])
            conf['host'] = u.hostname
            conf['port'] = u.port
        else:
            conf['host'] = "localhost"
            conf['port'] = "9000"

        if autodetect:
            return (conf['host'], conf['port'])

        if self.nn != conf['host']:
            msg = "Possible Namenode hostname mismatch.  Detected {}".format(conf['host'])
            raise HDFSConfigException(msg)

        if str(self.nn_port) != str(conf['port']):
            msg = "Possible Namenode port mismatch.  Detected {}".format(conf['port'])
            raise HDFSConfigException(msg)

        return conf

    def start(self, cmd, num_containers=1, virtual_cores=1, memory=128, env=""):
        """
        Method to start a yarn app with a distributed shell

        Parameters
        ----------
        cmd: str
            command to run in each yarn container
        num_containers: int
            number of containers to start (default 1)
        num_containers: int
            Number of containers YARN should request (default: 1)
            * A container should be requested with the number of cores it can saturate, i.e.
            * the average number of threads it expects to have runnable at a time.
        virtual_cores: int
            Number of virtual cores per container (default: 1)
            * A node's capacity should be configured with virtual cores equal to
            * its number of physical cores.
        memory: int
            Memory per container (default: 128)
            * The unit for memory is megabytes.
        env: string
            Full Path to zipped Python environment

        Returns
        -------
        applicationId: str
            A yarn application ID string
        """

        args = ["hadoop", "jar", self.JAR_FILE_PATH, JAVA_APP, "--numContainers", str(num_containers),
                "--command", cmd, "--virtualCores", str(virtual_cores), "--memory", str(memory)]

        if env:
            args = args + ["--pythonEnv", str(env)]

        logger.debug("Running Command: {}".format(' '.join(args)))
        proc = Popen(args, stdout=PIPE, stderr=PIPE)
        out, err = proc.communicate()

        logger.debug(out)
        logger.debug(err)

        # last string in out is applicationId
        # TODO Better JAVA Python communcation: appId, Resources, Yarn, etc.

        appId = out.split()[-1].decode("utf-8")
        appId = re.sub('id', '', appId)
        return appId

    def logs(self, app_id, shell=False):
        """
        Collect logs from each container

        Parameters
        ----------
        app_id: str
             A yarn application ID string
        shell: bool
             Shell out to yarn CLI (default False)

        Returns
        -------
        log: dictionary
            logs from each container
        """

        if shell:

            args = ["yarn", "logs", "-applicationId", app_id]

            proc = Popen(args, stdout=PIPE, stderr=PIPE)
            out, err = proc.communicate()

            logger.debug(out)
            logger.debug(err)
            return str(out)

        host_port = "{}:{}".format(self.rm, self.rm_port)
        url = "http://{}/ws/v1/cluster/apps/{}".format(host_port, app_id)
        logger.debug("Getting Resource Manager Info: {}".format(url))
        r = requests.get(url)
        data = r.json()
        logger.debug(data)

        try:
            amHostHttpAddress = data['app']['amHostHttpAddress']
        except KeyError:
            msg = "Local logs unavailable. State: {} finalStatus: {} Possibly check logs " \
                  "with `yarn logs -applicationId`".format(data['app']['state'],
                                                           data['app']['finalStatus'])
            raise Exception(msg)

        url = "http://{}/ws/v1/node/containers".format(amHostHttpAddress)
        r = requests.get(url)
        data = r.json()['containers']['container']
        logger.debug(data)

        # container_1452274436693_0001_01_000001
        def get_app_id_num(x):
            return "_".join(x.split("_")[1:3])

        app_id_num = get_app_id_num(app_id)
        containers = [d for d in data if get_app_id_num(d['id']) == app_id_num]

        logs = {}
        for c in containers:
            log = {}
            log['nodeId'] = c['nodeId']

            # grab stdout
            url = "{}/stdout/?start=0".format(c['containerLogsLink'])
            logger.debug("Gather stdout/stderr data from {}: {}".format(c['nodeId'], url))
            r = requests.get(url)
            log['stdout'] = r.text

            # grab stderr
            url = "{}/stderr/?start=0".format(c['containerLogsLink'])
            r = requests.get(url)
            log['stderr'] = r.text

            logs[c['id']] = log

        return logs

    def status(self, app_id):
        """ Get status of an application

        Parameters
        ----------
        app_id: str
             A yarn application ID string

        Returns
        -------
        log: dictionary
            status of application
        """
        host_port = "{}:{}".format(self.rm, self.rm_port)
        url = "http://{}/ws/v1/cluster/apps/{}".format(host_port, app_id)
        logger.debug("Getting Application Info: {}".format(url))
        r = requests.get(url)
        data = r.json()

        return data

    def kill(self, application_id):
        """
        Method to kill a yarn application

        Parameters
        ----------
        application_id: str
            YARN applicaiton id

        Returns
        -------
        bool:
            True if successful, False otherwise.
        """

        args = ["yarn", "application", "-kill", application_id]

        proc = Popen(args, stdout=PIPE, stderr=PIPE)
        out, err = proc.communicate()

        logger.debug(out)
        logger.debug(err)

        return any("Killed application" in s for s in [str(out), str(err)])

    @staticmethod
    def create_env(env_name, packages=None, conda_root=None, remove=False):
        """
        Create zipped directory of a conda environment

        Parameters
        ----------
        env_name : str
        packages : list
        conda_root : str, optional
        remove : bool
            remove possible conda environment before creating

        Returns
        -------
        path: str
            path to zipped conda environment

        Examples
        --------

        >>> k = Knit()
        >>> pkg_path = k.create_env(env_name='dev', packages=[python=3, numpy, distributed])
        """

        c = CondaCreator(conda_root=conda_root)
        path = c.create_env(env_name, packages=packages, remove=remove)

        return path

<|endoftext|>


<commit_before>from PyQt5 import QtWidgets, uic
from PyQt5.QtWidgets import QTableWidgetItem, QMessageBox
from PyQt5.QtGui import QPen, QColor, QImage, QPixmap, QPainter, QTransform
from PyQt5.QtCore import Qt, QTime, QCoreApplication, QEventLoop, QPoint
import time

red = Qt.red
blue = Qt.blue
now = None

class Window(QtWidgets.QMainWindow):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        uic.loadUi("window.ui", self)
        self.scene = Scene(0, 0, 561, 581)
        self.scene.win = self
        self.view.setScene(self.scene)
        self.image = QImage(561, 581, QImage.Format_ARGB32_Premultiplied)
        self.image.fill(Qt.white)
        self.bars.clicked.connect(lambda : set_bars(self))
        self.erase.clicked.connect(lambda: clean_all(self))
        self.paint.clicked.connect(lambda: clipping(self))
        self.rect.clicked.connect(lambda: set_rect(self))
        self.ect.clicked.connect(lambda: add_bars(self))
        self.lines = []
        self.clip = None
        self.point_now = None
        self.input_bars = False
        self.input_rect = False
        self.pen = QPen(red)

class Scene(QtWidgets.QGraphicsScene):

    def mousePressEvent(self, event):
        add_point(event.scenePos())

    def mouseMoveEvent(self, event):
        global now, w
        if w.input_rect:
            if now is None:
                now = event.scenePos()
            else:
                self.removeItem(self.itemAt(now, QTransform()))
                p = event.scenePos()
                self.addRect(now.x(), now.y(), abs(now.x() - p.x()), abs(now.y() - p.y()))

def set_bars(win):
    if win.input_bars:
        win.input_bars = False
        win.rect.setDisabled(False)
        win.erase.setDisabled(False)
        win.paint.setDisabled(False)
        win.ect.setDisabled(False)
    else:
        win.input_bars = True
        win.rect.setDisabled(True)
        win.erase.setDisabled(True)
        win.paint.setDisabled(True)
        win.ect.setDisabled(True)

def set_rect(win):
    if win.input_rect:
        win.input_rect = False
        win.bars.setDisabled(False)
        win.erase.setDisabled(False)
        win.paint.setDisabled(False)
        win.ect.setDisabled(False)
    else:
        win.input_rect = True
        win.bars.setDisabled(True)
        win.erase.setDisabled(True)
        win.paint.setDisabled(True)
        win.ect.setDisabled(True)

def add_row(win):
    win.table.insertRow(win.table.rowCount())

def add_point(point):
    global w
    if w.input_bars:
        if w.point_now is None:
            w.point_now = point
        else:
            w.lines.append([[w.point_now.x(), w.point_now.y()],
                            [point.x(), point.y()]])

            add_row(w)
            i = w.table.rowCount() - 1
            item_b = QTableWidgetItem("[{0}, {1}]".format(w.point_now.x(), w.point_now.y()))
            item_e = QTableWidgetItem("[{0}, {1}]".format(point.x(), point.y()))
            w.table.setItem(i, 0, item_b)
            w.table.setItem(i, 1, item_e)
            w.scene.addLine(w.point_now.x(), w.point_now.y(), point.x(), point.y(), w.pen)
            w.point_now = None

def clean_all(win):
    win.scene.clear()
    win.table.clear()
    win.lines = []
    win.image.fill(Qt.white)
    r = win.table.rowCount()
    for i in range(r, -1, -1):
        win.table.removeRow(i)

def add_bars(win):
    global now
    if now is None:
        QMessageBox.warning(win, "Внимание!", "Не введен отсекатель!")
        return
    buf = win.scene.itemAt(now, QTransform())
    if buf is None:
        QMessageBox.warning(win, "Внимание!", "Не введен отсекатель!")
    else:
        buf = buf.rect()
        win.clip = [buf.left(), buf.right(), buf.top(),  buf.bottom()]

        t = abs(win.clip[2] - win.clip[3]) * 0.8
        k = abs(win.clip[0] - win.clip[1]) * 0.8
        # задаем граничные отрезки
        win.pen.setColor(red)
        w.lines.append([[win.clip[0], win.clip[2] + t],  [win.clip[0], win.clip[3] - t]])
        add_row(w)
        i = w.table.rowCount() - 1
        item_b = QTableWidgetItem("[{0}, {1}]".format(win.clip[0], win.clip[2] + t))
        item_e = QTableWidgetItem("[{0}, {1}]".format(win.clip[0], win.clip[3] - t))
        w.table.setItem(i, 0, item_b)
        w.table.setItem(i, 1, item_e)
        win.scene.addLine(win.clip[0], win.clip[2] + t,  win.clip[0], win.clip[3] - t, win.pen)

        w.lines.append([[win.clip[1], win.clip[2] + t],  [win.clip[1], win.clip[3] - t]])
        add_row(w)
        i = w.table.rowCount() - 1
        item_b = QTableWidgetItem("[{0}, {1}]".format(win.clip[1], win.clip[2] + t))
        item_e = QTableWidgetItem("[{0}, {1}]".format(win.clip[1], win.clip[3] - t))
        w.table.setItem(i, 0, item_b)
        w.table.setItem(i, 1, item_e)
        win.scene.addLine(win.clip[1], win.clip[3] - t,  win.clip[1], win.clip[2] + t, win.pen)

        w.lines.append([[win.clip[0] + k, win.clip[2]], [win.clip[1] - k, win.clip[2]]])
        add_row(w)
        i = w.table.rowCount() - 1
        item_b = QTableWidgetItem("[{0}, {1}]".format(win.clip[0] + k, win.clip[2]))
        item_e = QTableWidgetItem("[{0}, {1}]".format(win.clip[1] - k, win.clip[2]))
        w.table.setItem(i, 0, item_b)
        w.table.setItem(i, 1, item_e)
        win.scene.addLine(win.clip[0] + k, win.clip[2], win.clip[1] - k, win.clip[2], win.pen)

        w.lines.append([[win.clip[0] + k, win.clip[3]], [win.clip[1] - k, win.clip[3]]])
        add_row(w)
        i = w.table.rowCount() - 1
        item_b = QTableWidgetItem("[{0}, {1}]".format(win.clip[0] + k, win.clip[3]))
        item_e = QTableWidgetItem("[{0}, {1}]".format(win.clip[1] - k, win.clip[3]))
        w.table.setItem(i, 0, item_b)
        w.table.setItem(i, 1, item_e)
        win.scene.addLine(win.clip[0] + k, win.clip[3], win.clip[1] - k, win.clip[3], win.pen)

def get_code(a, rect):
    code = [0, 0, 0, 0]
    if a[0] < rect[0]:
        code[3] = 1
    if a[0] > rect[1]:
        code[2] = 1
    if a[1] < rect[2]:
        code[1] = 1
    if a[1] > rect[3]:
        code[0] = 1

    return code

def clipping(win):
    buf = win.scene.itemAt(now, QTransform()).rect()
    win.clip = [buf.left(), buf.right(), buf.top(),  buf.bottom()]
    for b in win.lines:
        pass
        win.pen.setColor(blue)
        cohen_sutherland(b, win.clip, win)
        win.pen.setColor(red)

def log_prod(code1, code2):
    p = 0
    for i in range(4):
        p += code1[i] & code2[i]

    return p

def is_visible(bar, rect):
    """Видимость - 0 = невидимый
                   1 = видимый
                   2 = частично видимый"""
    # вычисление кодов концевых точек отрезка
    s1 = sum(get_code(bar[0], rect))
    s2 = sum(get_code(bar[1], rect))

    # предположим, что отрезок частично видим
    vis = 2

    # проверка полной видимости отрезка
    if not s1 and not s2:
        vis = 1
    else:
        # проверка тривиальной невидимости отрезка
        l = log_prod(get_code(bar[0], rect), get_code(bar[1], rect))
        if l != 0:
            vis = 0

    return vis

def cohen_sutherland(bar, rect, win):
    # инициализация флага
    flag = 1
    t = 1 # общего положения

    # проверка вертикальности и горизонтальности отрезка
    if bar[1][0] - bar[0][0] == 0:
        flag = -1   # вертикальный отрезок
    else:
        # вычисление наклона
        t = (bar[1][1] - bar[0][1]) / (bar[1][0] - bar[0][0])
        if t == 0:
            flag = 0   # горизонтальный

    # для каждой стороны окна
    for i in range(4):
        vis = is_visible(bar, rect)
        if vis == 1:
            win.scene.addLine(bar[0][0], bar[0][1], bar[1][0], bar[1][1], win.pen)
            return
        elif not vis:
            return

        # проверка пересечения отрезка и стороны окнай
        code1 = get_code(bar[0], rect)
        code2 = get_code(bar[1], rect)
        if code1[3 - i] == code2[3 - i]:
            continue

        # проверка нахождения Р1 вне окна; если Р1 внутри окна, то Р2 и Р1 поменять местами
        if not code1[3 - i]:
            bar[0], bar[1] = bar[1], bar[0]

        # поиск пересечений отрезка со сторонами окна
        # контроль вертикальности отрезка
        if flag != -1 and i < 2:
            bar[0][1] = t * (rect[i] - bar[0][0]) + bar[0][1]
            bar[0][0] = rect[i]
        else:
            if flag != 0:
                if flag != -1:
                    bar[0][0] = (1 / t) * (rect[i] - bar[0][1]) + bar[0][0]
                bar[0][1] = rect[i]
    win.scene.addLine(bar[0][0], bar[0][1], bar[1][0], bar[1][1], win.pen)

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = Window()
    w.show()
    sys.exit(app.exec_())
<commit_msg>update algorithm<commit_after>from PyQt5 import QtWidgets, uic
from PyQt5.QtWidgets import QTableWidgetItem, QMessageBox
from PyQt5.QtGui import QPen, QColor, QImage, QPixmap, QPainter, QTransform
from PyQt5.QtCore import Qt, QTime, QCoreApplication, QEventLoop, QPoint
import time

red = Qt.red
blue = Qt.blue
now = None

class Window(QtWidgets.QMainWindow):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        uic.loadUi("window.ui", self)
        self.scene = Scene(0, 0, 561, 581)
        self.scene.win = self
        self.view.setScene(self.scene)
        self.image = QImage(561, 581, QImage.Format_ARGB32_Premultiplied)
        self.image.fill(Qt.white)
        self.bars.clicked.connect(lambda : set_bars(self))
        self.erase.clicked.connect(lambda: clean_all(self))
        self.paint.clicked.connect(lambda: clipping(self))
        self.rect.clicked.connect(lambda: set_rect(self))
        self.ect.clicked.connect(lambda: add_bars(self))
        self.lines = []
        self.clip = None
        self.point_now = None
        self.input_bars = False
        self.input_rect = False
        self.pen = QPen(red)

class Scene(QtWidgets.QGraphicsScene):

    def mousePressEvent(self, event):
        add_point(event.scenePos())

    def mouseMoveEvent(self, event):
        global now, w
        if w.input_rect:
            if now is None:
                now = event.scenePos()
            else:
                self.removeItem(self.itemAt(now, QTransform()))
                p = event.scenePos()
                self.addRect(now.x(), now.y(), abs(now.x() - p.x()), abs(now.y() - p.y()))

def set_bars(win):
    if win.input_bars:
        win.input_bars = False
        win.rect.setDisabled(False)
        win.erase.setDisabled(False)
        win.paint.setDisabled(False)
        win.ect.setDisabled(False)
    else:
        win.input_bars = True
        win.rect.setDisabled(True)
        win.erase.setDisabled(True)
        win.paint.setDisabled(True)
        win.ect.setDisabled(True)

def set_rect(win):
    if win.input_rect:
        win.input_rect = False
        win.bars.setDisabled(False)
        win.erase.setDisabled(False)
        win.paint.setDisabled(False)
        win.ect.setDisabled(False)
    else:
        win.input_rect = True
        win.bars.setDisabled(True)
        win.erase.setDisabled(True)
        win.paint.setDisabled(True)
        win.ect.setDisabled(True)

def add_row(win):
    win.table.insertRow(win.table.rowCount())

def add_point(point):
    global w
    if w.input_bars:
        if w.point_now is None:
            w.point_now = point
        else:
            w.lines.append([[w.point_now.x(), w.point_now.y()],
                            [point.x(), point.y()]])

            add_row(w)
            i = w.table.rowCount() - 1
            item_b = QTableWidgetItem("[{0}, {1}]".format(w.point_now.x(), w.point_now.y()))
            item_e = QTableWidgetItem("[{0}, {1}]".format(point.x(), point.y()))
            w.table.setItem(i, 0, item_b)
            w.table.setItem(i, 1, item_e)
            w.scene.addLine(w.point_now.x(), w.point_now.y(), point.x(), point.y(), w.pen)
            w.point_now = None

def clean_all(win):
    win.scene.clear()
    win.table.clear()
    win.lines = []
    win.image.fill(Qt.white)
    r = win.table.rowCount()
    for i in range(r, -1, -1):
        win.table.removeRow(i)

def add_bars(win):
    global now
    if now is None:
        QMessageBox.warning(win, "Внимание!", "Не введен отсекатель!")
        return
    buf = win.scene.itemAt(now, QTransform())
    if buf is None:
        QMessageBox.warning(win, "Внимание!", "Не введен отсекатель!")
    else:
        buf = buf.rect()
        win.clip = [buf.left(), buf.right(), buf.top(),  buf.bottom()]

        t = abs(win.clip[2] - win.clip[3]) * 0.8
        k = abs(win.clip[0] - win.clip[1]) * 0.8
        # задаем граничные отрезки
        win.pen.setColor(red)
        w.lines.append([[win.clip[0], win.clip[2] + t],  [win.clip[0], win.clip[3] - t]])
        add_row(w)
        i = w.table.rowCount() - 1
        item_b = QTableWidgetItem("[{0}, {1}]".format(win.clip[0], win.clip[2] + t))
        item_e = QTableWidgetItem("[{0}, {1}]".format(win.clip[0], win.clip[3] - t))
        w.table.setItem(i, 0, item_b)
        w.table.setItem(i, 1, item_e)
        win.scene.addLine(win.clip[0], win.clip[2] + t,  win.clip[0], win.clip[3] - t, win.pen)

        w.lines.append([[win.clip[1], win.clip[2] + t],  [win.clip[1], win.clip[3] - t]])
        add_row(w)
        i = w.table.rowCount() - 1
        item_b = QTableWidgetItem("[{0}, {1}]".format(win.clip[1], win.clip[2] + t))
        item_e = QTableWidgetItem("[{0}, {1}]".format(win.clip[1], win.clip[3] - t))
        w.table.setItem(i, 0, item_b)
        w.table.setItem(i, 1, item_e)
        win.scene.addLine(win.clip[1], win.clip[3] - t,  win.clip[1], win.clip[2] + t, win.pen)

        w.lines.append([[win.clip[0] + k, win.clip[2]], [win.clip[1] - k, win.clip[2]]])
        add_row(w)
        i = w.table.rowCount() - 1
        item_b = QTableWidgetItem("[{0}, {1}]".format(win.clip[0] + k, win.clip[2]))
        item_e = QTableWidgetItem("[{0}, {1}]".format(win.clip[1] - k, win.clip[2]))
        w.table.setItem(i, 0, item_b)
        w.table.setItem(i, 1, item_e)
        win.scene.addLine(win.clip[0] + k, win.clip[2], win.clip[1] - k, win.clip[2], win.pen)

        w.lines.append([[win.clip[0] + k, win.clip[3]], [win.clip[1] - k, win.clip[3]]])
        add_row(w)
        i = w.table.rowCount() - 1
        item_b = QTableWidgetItem("[{0}, {1}]".format(win.clip[0] + k, win.clip[3]))
        item_e = QTableWidgetItem("[{0}, {1}]".format(win.clip[1] - k, win.clip[3]))
        w.table.setItem(i, 0, item_b)
        w.table.setItem(i, 1, item_e)
        win.scene.addLine(win.clip[0] + k, win.clip[3], win.clip[1] - k, win.clip[3], win.pen)

def get_code(a, rect):
    code = [0, 0, 0, 0]
    if a[0] < rect[0]:
        code[0] = 1
    if a[0] > rect[1]:
        code[1] = 1
    if a[1] < rect[2]:
        code[2] = 1
    if a[1] > rect[3]:
        code[3] = 1

    return code

def clipping(win):
    buf = win.scene.itemAt(now, QTransform()).rect()
    win.clip = [buf.left(), buf.right(), buf.top(),  buf.bottom()]
    for b in win.lines:
        pass
        win.pen.setColor(blue)
        cohen_sutherland(b, win.clip, win)
        win.pen.setColor(red)

def log_prod(code1, code2):
    p = 0
    for i in range(4):
        p += code1[i] & code2[i]

    return p

def is_visible(bar, rect):
    """Видимость - 0 = невидимый
                   1 = видимый
                   2 = частично видимый"""
    # вычисление кодов концевых точек отрезка
    s1 = sum(get_code(bar[0], rect))
    s2 = sum(get_code(bar[1], rect))

    # предположим, что отрезок частично видим
    vis = 2

    # проверка полной видимости отрезка
    if not s1 and not s2:
        vis = 1
    else:
        # проверка тривиальной невидимости отрезка
        l = log_prod(get_code(bar[0], rect), get_code(bar[1], rect))
        if l != 0:
            vis = 0

    return vis

def cohen_sutherland(bar, rect, win):
    # инициализация флага
    flag = 1 # общего положения
    t = 1

    # проверка вертикальности и горизонтальности отрезка
    if bar[1][0] - bar[0][0] == 0:
        flag = -1   # вертикальный отрезок
    else:
        # вычисление наклона
        t = (bar[1][1] - bar[0][1]) / (bar[1][0] - bar[0][0])
        if t == 0:
            flag = 0   # горизонтальный

    # для каждой стороны окна
    for i in range(4):
        vis = is_visible(bar, rect)
        if vis == 1:
            win.scene.addLine(bar[0][0], bar[0][1], bar[1][0], bar[1][1], win.pen)
            return
        elif not vis:
            return

        # проверка пересечения отрезка и стороны окна
        code1 = get_code(bar[0], rect)
        code2 = get_code(bar[1], rect)

        if code1[i] == code2[i]:
            continue

        # проверка нахождения Р1 вне окна; если Р1 внутри окна, то Р2 и Р1 поменять местами
        if not code1[i]:
            bar[0], bar[1] = bar[1], bar[0]

        # поиск пересечений отрезка со сторонами окна
        # контроль вертикальности отрезка
        if flag != -1:
            if i < 2:
                bar[0][1] = t * (rect[i] - bar[0][0]) + bar[0][1]
                bar[0][0] = rect[i]
                continue
            else:
                bar[0][0] = (1 / t) * (rect[i] - bar[0][1]) + bar[0][0]

        bar[0][1] = rect[i]
    win.scene.addLine(bar[0][0], bar[0][1], bar[1][0], bar[1][1], win.pen)

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = Window()
    w.show()
    sys.exit(app.exec_())
<|endoftext|>


<commit_before><commit_msg>[FIX] tests: close Chrome and shutdown on SIGXCPU<commit_after><|endoftext|>


<commit_before>from __future__ import absolute_import

import os
import time
from collections import defaultdict
from io import open

from .utils import STRING_TYPE
from .load_grammar import load_grammar
from .tree import Tree
from .common import LexerConf, ParserConf

from .lexer import Lexer, TraditionalLexer
from .parse_tree_builder import ParseTreeBuilder
from .parser_frontends import get_frontend

class LarkOptions(object):
    """Specifies the options for Lark

    """
    OPTIONS_DOC = """
        parser - Decides which parser engine to use, "earley" or "lalr". (Default: "earley")
                 Note: "lalr" requires a lexer

        lexer - Decides whether or not to use a lexer stage
            "standard": Use a standard lexer
            "contextual": Stronger lexer (only works with parser="lalr")
            "dynamic": Flexible and powerful (only with parser="earley")
            "dynamic_complete": Same as dynamic, but tries *every* variation
                                of tokenizing possible. (only with parser="earley")
            "auto" (default): Choose for me based on grammar and parser

        ambiguity - Decides how to handle ambiguity in the parse. Only relevant if parser="earley"
            "resolve": The parser will automatically choose the simplest derivation
                       (it chooses consistently: greedy for tokens, non-greedy for rules)
            "explicit": The parser will return all derivations wrapped in "_ambig" tree nodes (i.e. a forest).

        transformer - Applies the transformer to every parse tree
        debug - Affects verbosity (default: False)
        keep_all_tokens - Don't automagically remove "punctuation" tokens (default: False)
        cache_grammar - Cache the Lark grammar (Default: False)
        postlex - Lexer post-processing (Default: None) Only works with the standard and contextual lexers.
        start - The start symbol (Default: start)
        profile - Measure run-time usage in Lark. Read results from the profiler proprety (Default: False)
        priority - How priorities should be evaluated - auto, none, normal, invert (Default: auto)
        propagate_positions - Propagates [line, column, end_line, end_column] attributes into all tree branches.
        lexer_callbacks - Dictionary of callbacks for the lexer. May alter tokens during lexing. Use with caution.
        maybe_placeholders - Experimental feature. Instead of omitting optional rules (i.e. rule?), replace them with None
    """
    if __doc__:
        __doc__ += OPTIONS_DOC

    _defaults = {
        'debug': False,
        'keep_all_tokens': False,
        'tree_class': Tree,
        'cache_grammar': False,
        'postlex': None,
        'parser': 'earley',
        'lexer': 'auto',
        'transformer': None,
        'start': 'start',
        'profile': False,
        'priority': 'auto',
        'ambiguity': 'auto',
        'propagate_positions': False,
        'lexer_callbacks': {},
        'maybe_placeholders': False,
    }

    def __init__(self, options_dict):
        o = dict(options_dict)

        options = {}
        for name, default in self._defaults.items():
            if name in o:
                value = o.pop(name)
                if isinstance(default, bool):
                    value = bool(value)
            else:
                value = default

            options[name] = value

        self.__dict__['options'] = options

        assert self.parser in ('earley', 'lalr', 'cyk', None)

        if self.parser == 'earley' and self.transformer:
            raise ValueError('Cannot specify an embedded transformer when using the Earley algorithm.'
                             'Please use your transformer on the resulting parse tree, or use a different algorithm (i.e. LALR)')

        if o:
            raise ValueError("Unknown options: %s" % o.keys())

    def __getattr__(self, name):
        return self.options[name]
    def __setattr__(self, name, value):
        self.options[name] = value

    def serialize(self):
        return self.options

    @classmethod
    def deserialize(cls, data):
        return cls(data)

class Profiler:
    def __init__(self):
        self.total_time = defaultdict(float)
        self.cur_section = '__init__'
        self.last_enter_time = time.time()

    def enter_section(self, name):
        cur_time = time.time()
        self.total_time[self.cur_section] += cur_time - self.last_enter_time
        self.last_enter_time = cur_time
        self.cur_section = name

    def make_wrapper(self, name, f):
        def wrapper(*args, **kwargs):
            last_section = self.cur_section
            self.enter_section(name)
            try:
                return f(*args, **kwargs)
            finally:
                self.enter_section(last_section)

        return wrapper

class Lark:
    def __init__(self, grammar, **options):
        """
            grammar : a string or file-object containing the grammar spec (using Lark's ebnf syntax)
            options : a dictionary controlling various aspects of Lark.
        """
        self.options = LarkOptions(options)

        # Some, but not all file-like objects have a 'name' attribute
        try:
            self.source = grammar.name
        except AttributeError:
            self.source = '<string>'

        # Drain file-like objects to get their contents
        try:
            read = grammar.read
        except AttributeError:
            pass
        else:
            grammar = read()

        assert isinstance(grammar, STRING_TYPE)

        if self.options.cache_grammar:
            raise NotImplementedError("Not available yet")

        assert not self.options.profile, "Feature temporarily disabled"
        # self.profiler = Profiler() if self.options.profile else None

        if self.options.lexer == 'auto':
            if self.options.parser == 'lalr':
                self.options.lexer = 'contextual'
            elif self.options.parser == 'earley':
                self.options.lexer = 'dynamic'
            elif self.options.parser == 'cyk':
                self.options.lexer = 'standard'
            else:
                assert False, self.options.parser
        lexer = self.options.lexer
        assert lexer in ('standard', 'contextual', 'dynamic', 'dynamic_complete') or issubclass(lexer, Lexer)

        if self.options.ambiguity == 'auto':
            if self.options.parser == 'earley':
                self.options.ambiguity = 'resolve'
        else:
            disambig_parsers = ['earley', 'cyk']
            assert self.options.parser in disambig_parsers, (
                'Only %s supports disambiguation right now') % ', '.join(disambig_parsers)

        if self.options.priority == 'auto':
            if self.options.parser in ('earley', 'cyk', ):
                self.options.priority = 'normal'
            elif self.options.parser in ('lalr', ):
                self.options.priority = None
        elif self.options.priority in ('invert', 'normal'):
            assert self.options.parser in ('earley', 'cyk'), "priorities are not supported for LALR at this time"

        assert self.options.priority in ('auto', None, 'normal', 'invert'), 'invalid priority option specified: {}. options are auto, none, normal, invert.'.format(self.options.priority)
        assert self.options.ambiguity not in ('resolve__antiscore_sum', ), 'resolve__antiscore_sum has been replaced with the option priority="invert"'
        assert self.options.ambiguity in ('resolve', 'explicit', 'auto', )

        # Parse the grammar file and compose the grammars (TODO)
        self.grammar = load_grammar(grammar, self.source)

        # Compile the EBNF grammar into BNF
        self.terminals, self.rules, self.ignore_tokens = self.grammar.compile()

        # If the user asked to invert the priorities, negate them all here.
        # This replaces the old 'resolve__antiscore_sum' option.
        if self.options.priority == 'invert':
            for rule in self.rules:
                if rule.options and rule.options.priority is not None:
                    rule.options.priority = -rule.options.priority
        # Else, if the user asked to disable priorities, strip them from the
        # rules. This allows the Earley parsers to skip an extra forest walk
        # for improved performance, if you don't need them (or didn't specify any).
        elif self.options.priority == None:
            for rule in self.rules:
                if rule.options and rule.options.priority is not None:
                    rule.options.priority = None
        self.lexer_conf = LexerConf(self.terminals, self.ignore_tokens, self.options.postlex, self.options.lexer_callbacks)

        if self.options.parser:
            self.parser = self._build_parser()
        elif lexer:
            self.lexer = self._build_lexer()

    if __init__.__doc__:
        __init__.__doc__ += "\nOPTIONS:" + LarkOptions.OPTIONS_DOC

    def _build_lexer(self):
        return TraditionalLexer(self.lexer_conf.tokens, ignore=self.lexer_conf.ignore, user_callbacks=self.lexer_conf.callbacks)

    def _build_parser(self):
        self.parser_class = get_frontend(self.options.parser, self.options.lexer)

        self._parse_tree_builder = ParseTreeBuilder(self.rules, self.options.tree_class, self.options.propagate_positions, self.options.keep_all_tokens, self.options.parser!='lalr' and self.options.ambiguity=='explicit', self.options.maybe_placeholders)
        callbacks = self._parse_tree_builder.create_callback(self.options.transformer)

        parser_conf = ParserConf(self.rules, callbacks, self.options.start)

        return self.parser_class(self.lexer_conf, parser_conf, options=self.options)

    def serialize(self):
        return {
            'parser': self.parser.serialize(),
            'rules': [r.serialize() for r in self.rules],
            'options': self.options.serialize(),
        }

    @classmethod
    def deserialize(cls, data):
        from .grammar import Rule
        inst = cls.__new__(cls)

        rules = [Rule.deserialize(r) for r in data['rules']]
        options = LarkOptions.deserialize(data['options'])

        ptb = ParseTreeBuilder(rules, options.tree_class, options.propagate_positions, options.keep_all_tokens, options.parser!='lalr' and options.ambiguity=='explicit', options.maybe_placeholders)
        callbacks = ptb.create_callback(None)

        parser_class = get_frontend(options.parser, options.lexer)
        inst.parser = parser_class.deserialize(data['parser'], callbacks)
        return inst

    @classmethod
    def open(cls, grammar_filename, rel_to=None, **options):
        """Create an instance of Lark with the grammar given by its filename

        If rel_to is provided, the function will find the grammar filename in relation to it.

        Example:

            >>> Lark.open("grammar_file.lark", rel_to=__file__, parser="lalr")
            Lark(...)

        """
        if rel_to:
            basepath = os.path.dirname(rel_to)
            grammar_filename = os.path.join(basepath, grammar_filename)
        with open(grammar_filename, encoding='utf8') as f:
            return cls(f, **options)

    def __repr__(self):
        return 'Lark(open(%r), parser=%r, lexer=%r, ...)' % (self.source, self.options.parser, self.options.lexer)

    def lex(self, text):
        "Only lex (and postlex) the text, without parsing it. Only relevant when lexer='standard'"
        if not hasattr(self, 'lexer'):
            self.lexer = self._build_lexer()
        stream = self.lexer.lex(text)
        if self.options.postlex:
            return self.options.postlex.process(stream)
        return stream

    def parse(self, text):
        "Parse the given text, according to the options provided. Returns a tree, unless specified otherwise."
        return self.parser.parse(text)

        # if self.profiler:
        #     self.profiler.enter_section('lex')
        #     l = list(self.lex(text))
        #     self.profiler.enter_section('parse')
        #     try:
        #         return self.parser.parse(l)
        #     finally:
        #         self.profiler.enter_section('outside_lark')
        # else:
        #     l = list(self.lex(text))
        #     return self.parser.parse(l)
<commit_msg>Small refactor<commit_after>from __future__ import absolute_import

import os
import time
from collections import defaultdict
from io import open

from .utils import STRING_TYPE
from .load_grammar import load_grammar
from .tree import Tree
from .common import LexerConf, ParserConf

from .lexer import Lexer, TraditionalLexer
from .parse_tree_builder import ParseTreeBuilder
from .parser_frontends import get_frontend

class LarkOptions(object):
    """Specifies the options for Lark

    """
    OPTIONS_DOC = """
        parser - Decides which parser engine to use, "earley" or "lalr". (Default: "earley")
                 Note: "lalr" requires a lexer

        lexer - Decides whether or not to use a lexer stage
            "standard": Use a standard lexer
            "contextual": Stronger lexer (only works with parser="lalr")
            "dynamic": Flexible and powerful (only with parser="earley")
            "dynamic_complete": Same as dynamic, but tries *every* variation
                                of tokenizing possible. (only with parser="earley")
            "auto" (default): Choose for me based on grammar and parser

        ambiguity - Decides how to handle ambiguity in the parse. Only relevant if parser="earley"
            "resolve": The parser will automatically choose the simplest derivation
                       (it chooses consistently: greedy for tokens, non-greedy for rules)
            "explicit": The parser will return all derivations wrapped in "_ambig" tree nodes (i.e. a forest).

        transformer - Applies the transformer to every parse tree
        debug - Affects verbosity (default: False)
        keep_all_tokens - Don't automagically remove "punctuation" tokens (default: False)
        cache_grammar - Cache the Lark grammar (Default: False)
        postlex - Lexer post-processing (Default: None) Only works with the standard and contextual lexers.
        start - The start symbol (Default: start)
        profile - Measure run-time usage in Lark. Read results from the profiler proprety (Default: False)
        priority - How priorities should be evaluated - auto, none, normal, invert (Default: auto)
        propagate_positions - Propagates [line, column, end_line, end_column] attributes into all tree branches.
        lexer_callbacks - Dictionary of callbacks for the lexer. May alter tokens during lexing. Use with caution.
        maybe_placeholders - Experimental feature. Instead of omitting optional rules (i.e. rule?), replace them with None
    """
    if __doc__:
        __doc__ += OPTIONS_DOC

    _defaults = {
        'debug': False,
        'keep_all_tokens': False,
        'tree_class': Tree,
        'cache_grammar': False,
        'postlex': None,
        'parser': 'earley',
        'lexer': 'auto',
        'transformer': None,
        'start': 'start',
        'profile': False,
        'priority': 'auto',
        'ambiguity': 'auto',
        'propagate_positions': False,
        'lexer_callbacks': {},
        'maybe_placeholders': False,
    }

    def __init__(self, options_dict):
        o = dict(options_dict)

        options = {}
        for name, default in self._defaults.items():
            if name in o:
                value = o.pop(name)
                if isinstance(default, bool):
                    value = bool(value)
            else:
                value = default

            options[name] = value

        self.__dict__['options'] = options

        assert self.parser in ('earley', 'lalr', 'cyk', None)

        if self.parser == 'earley' and self.transformer:
            raise ValueError('Cannot specify an embedded transformer when using the Earley algorithm.'
                             'Please use your transformer on the resulting parse tree, or use a different algorithm (i.e. LALR)')

        if o:
            raise ValueError("Unknown options: %s" % o.keys())

    def __getattr__(self, name):
        return self.options[name]
    def __setattr__(self, name, value):
        self.options[name] = value

    def serialize(self):
        return self.options

    @classmethod
    def deserialize(cls, data):
        return cls(data)

class Profiler:
    def __init__(self):
        self.total_time = defaultdict(float)
        self.cur_section = '__init__'
        self.last_enter_time = time.time()

    def enter_section(self, name):
        cur_time = time.time()
        self.total_time[self.cur_section] += cur_time - self.last_enter_time
        self.last_enter_time = cur_time
        self.cur_section = name

    def make_wrapper(self, name, f):
        def wrapper(*args, **kwargs):
            last_section = self.cur_section
            self.enter_section(name)
            try:
                return f(*args, **kwargs)
            finally:
                self.enter_section(last_section)

        return wrapper

class Lark:
    def __init__(self, grammar, **options):
        """
            grammar : a string or file-object containing the grammar spec (using Lark's ebnf syntax)
            options : a dictionary controlling various aspects of Lark.
        """
        self.options = LarkOptions(options)

        # Some, but not all file-like objects have a 'name' attribute
        try:
            self.source = grammar.name
        except AttributeError:
            self.source = '<string>'

        # Drain file-like objects to get their contents
        try:
            read = grammar.read
        except AttributeError:
            pass
        else:
            grammar = read()

        assert isinstance(grammar, STRING_TYPE)

        if self.options.cache_grammar:
            raise NotImplementedError("Not available yet")

        assert not self.options.profile, "Feature temporarily disabled"
        # self.profiler = Profiler() if self.options.profile else None

        if self.options.lexer == 'auto':
            if self.options.parser == 'lalr':
                self.options.lexer = 'contextual'
            elif self.options.parser == 'earley':
                self.options.lexer = 'dynamic'
            elif self.options.parser == 'cyk':
                self.options.lexer = 'standard'
            else:
                assert False, self.options.parser
        lexer = self.options.lexer
        assert lexer in ('standard', 'contextual', 'dynamic', 'dynamic_complete') or issubclass(lexer, Lexer)

        if self.options.ambiguity == 'auto':
            if self.options.parser == 'earley':
                self.options.ambiguity = 'resolve'
        else:
            disambig_parsers = ['earley', 'cyk']
            assert self.options.parser in disambig_parsers, (
                'Only %s supports disambiguation right now') % ', '.join(disambig_parsers)

        if self.options.priority == 'auto':
            if self.options.parser in ('earley', 'cyk', ):
                self.options.priority = 'normal'
            elif self.options.parser in ('lalr', ):
                self.options.priority = None
        elif self.options.priority in ('invert', 'normal'):
            assert self.options.parser in ('earley', 'cyk'), "priorities are not supported for LALR at this time"

        assert self.options.priority in ('auto', None, 'normal', 'invert'), 'invalid priority option specified: {}. options are auto, none, normal, invert.'.format(self.options.priority)
        assert self.options.ambiguity not in ('resolve__antiscore_sum', ), 'resolve__antiscore_sum has been replaced with the option priority="invert"'
        assert self.options.ambiguity in ('resolve', 'explicit', 'auto', )

        # Parse the grammar file and compose the grammars (TODO)
        self.grammar = load_grammar(grammar, self.source)

        # Compile the EBNF grammar into BNF
        self.terminals, self.rules, self.ignore_tokens = self.grammar.compile()

        # If the user asked to invert the priorities, negate them all here.
        # This replaces the old 'resolve__antiscore_sum' option.
        if self.options.priority == 'invert':
            for rule in self.rules:
                if rule.options and rule.options.priority is not None:
                    rule.options.priority = -rule.options.priority
        # Else, if the user asked to disable priorities, strip them from the
        # rules. This allows the Earley parsers to skip an extra forest walk
        # for improved performance, if you don't need them (or didn't specify any).
        elif self.options.priority == None:
            for rule in self.rules:
                if rule.options and rule.options.priority is not None:
                    rule.options.priority = None
        self.lexer_conf = LexerConf(self.terminals, self.ignore_tokens, self.options.postlex, self.options.lexer_callbacks)

        if self.options.parser:
            self.parser = self._build_parser()
        elif lexer:
            self.lexer = self._build_lexer()

    if __init__.__doc__:
        __init__.__doc__ += "\nOPTIONS:" + LarkOptions.OPTIONS_DOC

    def _build_lexer(self):
        return TraditionalLexer(self.lexer_conf.tokens, ignore=self.lexer_conf.ignore, user_callbacks=self.lexer_conf.callbacks)

    def _prepare_callbacks(self):
        self.parser_class = get_frontend(self.options.parser, self.options.lexer)
        self._parse_tree_builder = ParseTreeBuilder(self.rules, self.options.tree_class, self.options.propagate_positions, self.options.keep_all_tokens, self.options.parser!='lalr' and self.options.ambiguity=='explicit', self.options.maybe_placeholders)
        self._callbacks = self._parse_tree_builder.create_callback(self.options.transformer)

    def _build_parser(self):
        self._prepare_callbacks()
        parser_conf = ParserConf(self.rules, self._callbacks, self.options.start)
        return self.parser_class(self.lexer_conf, parser_conf, options=self.options)

    def serialize(self):
        return {
            'parser': self.parser.serialize(),
            'rules': [r.serialize() for r in self.rules],
            'options': self.options.serialize(),
        }

    @classmethod
    def deserialize(cls, data):
        from .grammar import Rule
        inst = cls.__new__(cls)
        inst.options = LarkOptions.deserialize(data['options'])
        inst.rules = [Rule.deserialize(r) for r in data['rules']]
        inst._prepare_callbacks()
        inst.parser = inst.parser_class.deserialize(data['parser'], inst._callbacks)
        return inst

    @classmethod
    def open(cls, grammar_filename, rel_to=None, **options):
        """Create an instance of Lark with the grammar given by its filename

        If rel_to is provided, the function will find the grammar filename in relation to it.

        Example:

            >>> Lark.open("grammar_file.lark", rel_to=__file__, parser="lalr")
            Lark(...)

        """
        if rel_to:
            basepath = os.path.dirname(rel_to)
            grammar_filename = os.path.join(basepath, grammar_filename)
        with open(grammar_filename, encoding='utf8') as f:
            return cls(f, **options)

    def __repr__(self):
        return 'Lark(open(%r), parser=%r, lexer=%r, ...)' % (self.source, self.options.parser, self.options.lexer)

    def lex(self, text):
        "Only lex (and postlex) the text, without parsing it. Only relevant when lexer='standard'"
        if not hasattr(self, 'lexer'):
            self.lexer = self._build_lexer()
        stream = self.lexer.lex(text)
        if self.options.postlex:
            return self.options.postlex.process(stream)
        return stream

    def parse(self, text):
        "Parse the given text, according to the options provided. Returns a tree, unless specified otherwise."
        return self.parser.parse(text)

        # if self.profiler:
        #     self.profiler.enter_section('lex')
        #     l = list(self.lex(text))
        #     self.profiler.enter_section('parse')
        #     try:
        #         return self.parser.parse(l)
        #     finally:
        #         self.profiler.enter_section('outside_lark')
        # else:
        #     l = list(self.lex(text))
        #     return self.parser.parse(l)
<|endoftext|>


<commit_before>import string

from queryable import queryable

from ohno.dungeon import monster
from ohno.dungeon.feature import feature
from ohno.dungeon.item import item

_tile_is_feature  = lambda t: t.glyph in '.}{#_<>]^|-~ \\'
_tile_is_item     = lambda t: t.glyph in '`0*$[%)(/?!"=+'
_tile_is_monster  = lambda t: t.glyph in (string.ascii_letters + "12345@'&;:")
# doors are handled in walkable()
_tile_is_walkable = lambda t: t.glyph in '.}{#<>^ '

class Tile(object):
    def __init__(self, level, idx):
        self.level = level
        self.ohno = self.level.ohno
        self.idx = idx

        self._walkable = False
        self.explored = False
        self.searched = 0

        self.feature = None
        self.items = []
        self.monster = None
        self.has_hero = None

        self._adjacent = None
        self._orthogonal = None
        self._horizontal = None
        self._vertical = None

    def __str__(self):
        return '<Tile %d G=%s E=%d W=%d S=%d D=%f>' % (
            self.idx,
            self.appearance.glyph if self.appearance else ' ',
            self.explored,
            int(self.walkable or 0),
            self.searched,
            self.distance_from_hero() if self.ohno.ai.pathing.is_uptodate()
                                      else -1
        )
    __repr__ = __str__

    def set(self, appearance):
        """
        Will be called by `ohno.dungeon.level.update` if the current glyph or
        color has been changed since since the last update.
        `appearance` is "newly copied" (i.e. it's safe to change)
        """
        if _tile_is_feature(appearance):
            # Spaces ("dark part of the room") can never be set to explored
            # unless we have been adjacent to the square yet. This logic is
            # found in ohno.dungeon.level.
            self.explored = self.explored or appearance.glyph != ' '
            # If it's the first time we're seeing the feature of this tile or if
            # it has changed (i.e. water can spread to nearby floortiles).
            if not self.feature or self.feature.appearance != appearance:
                self.set_feature(appearance)
            self.items = []
            self.has_hero = False
            self.set_monster(None)

        elif _tile_is_item(appearance):
            self.ohno.logger.tile('Itemtile! %r' % self)
            self._walkable = True
            self.has_hero = False
            self.set_monster(None)

            # If the appearance of the tile changes (i.e. something else
            # has dropped, or a monster picking up the topmost item),
            # we'll completely remove any previous examined items,
            # since the hero should reexamine the tile anyway.
            # If it stays the same, we'll simply assume nothing has changed.
            if not self.items or self.items[-1].appearance != appearance:
                self.ohno.logger.tile('New item!')
                self.items = [item.create(self, appearance)]
                # Since this tile might have new information,
                # we set explored to False. That way, it'll be easier for our AI
                # to know this square is interesting.
                self.explored = False
            self.ohno.logger.tile('Itemtile is now %r' % self)

        elif self == self.ohno.dungeon.curtile:
            self.ohno.logger.tile('Herotile! %r' % self)
            assert not self.has_hero
            self._walkable = True
            self.has_hero = True
            self.set_monster(None)
            if self.ohno.hero.appearance != appearance:
                # Not sure if we need this, but this seems like a good place to
                # check if we're polymorphed.
                self.ohno.hero.set_appearance(appearance)
            self.ohno.logger.tile('Herotile is now %r' % self)

        elif _tile_is_monster(appearance):
            # TODO: Might not be true if this is a stone giant standing on a
            #       boulder.
            self.ohno.logger.tile('Monstertile! %r' % self)
            self.has_hero = False
            self._walkable = True
            if (not self.monster) or self.monster.appearance != appearance:
                self.ohno.logger.tile('New monster!')
                self.set_monster(appearance)
            self.ohno.logger.tile('Monstertile is now %r' % self)

    def set_feature(self, appearance):
            self.feature = feature.create(self, appearance)
            self._walkable = (self.is_open_door() or
                                _tile_is_walkable(appearance))

    def set_monster(self, appearance):
        """Sets self.monster and updates Levle.monsters"""
        new_monster = None
        if appearance is not None:
            new_monster = monster.create(self, appearance)

        if new_monster == self.monster:
            return

        if self.monster:
            self.level.monsters.remove(self.monster)

        if new_monster:
            self.level.monsters.append(new_monster)
            # Sometimes, a monster can open a door and right on it in one turn,
            # this will confuse ohno as it will still think it's a door under
            # the monster.
            if self.feature_isa('Door'):
                self.feature = None

        self.monster = new_monster

    @property
    def appearance(self):
        """What does this tile look like right now?"""
        # This could be cached, but doing it like this works as a sanity check
        # aswell :-)
        if self.monster is not None:
            return self.monster.appearance
        elif self.has_hero:
            return self.ohno.hero.appearance
        elif self.items:
            return self.items[-1].appearance
        elif self.feature is not None:
            return self.feature.appearance
        else:
            return None

    def distance_from_hero(self):
        """
        Returns the distance from hero as calculated by ai.pathing.
        If the tile is unreachable, it will return float('inf').
        """
        assert self.ohno.ai.pathing.tick == self.ohno.tick
        return self.ohno.ai.pathing.dists[self.idx]

    def feature_isa(self, class_name):
        return self.feature_name == class_name

    def is_open_door(self):
        return (self.feature and
                self.feature.appearance.glyph in '-|' and
                self.feature.appearance.fg == 33)

    # Methods for iterating neighbors
    @queryable
    def adjacent(self):
        """Return the direct neighbors of this tile."""
        # Since ohno.dungeon.level.tiles never changes (it's even a tuple), we
        # can safely cache the result of this function the first time we run it.
        if self._adjacent is None:
            self._adjacent = []
            dirs = (
                (-1, -1), (-1, 0), (-1, 1),
                ( 1, -1), ( 1, 0), ( 1, 1),
                ( 0, -1), ( 0, 1)
            )
            x, y = self.idx % 80, self.idx / 80
            for x2, y2 in ((dir[0] + x, dir[1] + y) for dir in dirs):
                if 0 <= x2 < 80 and 0 <= y2 < 21:
                    self._adjacent.append(self.level.tiles[y2 * 80 + x2])
            self._adjacent = tuple(self._adjacent)
        assert len(self._adjacent) <= 8
        return self._adjacent

    @queryable
    def orthogonal(self):
        if self._orthogonal is None:
            self._orthogonal = tuple(n for n in self.adjacent()
                                            if abs(self.idx - n.idx) in (1, 80))
        assert len(self._orthogonal) <= 4
        return self._orthogonal

    @queryable
    def vertical(self):
        if self._vertical is None:
            self._vertical = tuple(o for o in self.orthogonal()
                                            if abs(self.idx - o.idx) == 1)
        assert len(self._vertical) <= 2
        return self._vertical

    @queryable
    def horizontal(self):
        if self._horizontal is None:
            self._horizontal = tuple(o for o in self.orthogonal()
                                            if abs(self.idx - o.idx) == 80)
        assert len(self._horizontal) <= 2
        return self._horizontal

    # Methods to make it simpler to query
    @property
    def is_wall(self):
        return (self.explored and self.feature and
                self.feature.appearance.glyph in '|- ' and
                self.feature.appearance.fg == 37)

    @property
    def is_hallway(self):
        return (self.explored and self.feature and
                self.feature.appearance.glyph == '#' and
                self.feature.appearance.fg == 37)

    @property
    def is_floor(self):
        return (self.explored and self.feature and
                self.feature.appearance.glyph == '.' and
                self.feature.appearance.fg == 37)

    @property
    def has_monster(self):
        return self.monster is not None

    @property
    def has_closed_door(self):
        return self.feature_isa('Door') and self.feature.closed

    @property
    def feature_name(self):
        return self.feature.__class__.__name__ if self.feature else 'Unknown'

    @property
    def reachable(self):
        assert self.ohno.ai.pathing.is_uptodate()
        return self.distance_from_hero() != float('inf')

    @property
    def walkable(self):
        # This might be called before the tile is initialized, so make sure we
        # consider the case where appearance is None
        return (self.appearance and
                self._walkable and
                not self.monster and
                self.appearance.glyph != '0')
<commit_msg>(Re last commit:) Forgot to check if the door was closed<commit_after>import string

from queryable import queryable

from ohno.dungeon import monster
from ohno.dungeon.feature import feature
from ohno.dungeon.item import item

_tile_is_feature  = lambda t: t.glyph in '.}{#_<>]^|-~ \\'
_tile_is_item     = lambda t: t.glyph in '`0*$[%)(/?!"=+'
_tile_is_monster  = lambda t: t.glyph in (string.ascii_letters + "12345@'&;:")
# doors are handled in walkable()
_tile_is_walkable = lambda t: t.glyph in '.}{#<>^ '

class Tile(object):
    def __init__(self, level, idx):
        self.level = level
        self.ohno = self.level.ohno
        self.idx = idx

        self._walkable = False
        self.explored = False
        self.searched = 0

        self.feature = None
        self.items = []
        self.monster = None
        self.has_hero = None

        self._adjacent = None
        self._orthogonal = None
        self._horizontal = None
        self._vertical = None

    def __str__(self):
        return '<Tile %d G=%s E=%d W=%d S=%d D=%f>' % (
            self.idx,
            self.appearance.glyph if self.appearance else ' ',
            self.explored,
            int(self.walkable or 0),
            self.searched,
            self.distance_from_hero() if self.ohno.ai.pathing.is_uptodate()
                                      else -1
        )
    __repr__ = __str__

    def set(self, appearance):
        """
        Will be called by `ohno.dungeon.level.update` if the current glyph or
        color has been changed since since the last update.
        `appearance` is "newly copied" (i.e. it's safe to change)
        """
        if _tile_is_feature(appearance):
            # Spaces ("dark part of the room") can never be set to explored
            # unless we have been adjacent to the square yet. This logic is
            # found in ohno.dungeon.level.
            self.explored = self.explored or appearance.glyph != ' '
            # If it's the first time we're seeing the feature of this tile or if
            # it has changed (i.e. water can spread to nearby floortiles).
            if not self.feature or self.feature.appearance != appearance:
                self.set_feature(appearance)
            self.items = []
            self.has_hero = False
            self.set_monster(None)

        elif _tile_is_item(appearance):
            self.ohno.logger.tile('Itemtile! %r' % self)
            self._walkable = True
            self.has_hero = False
            self.set_monster(None)

            # If the appearance of the tile changes (i.e. something else
            # has dropped, or a monster picking up the topmost item),
            # we'll completely remove any previous examined items,
            # since the hero should reexamine the tile anyway.
            # If it stays the same, we'll simply assume nothing has changed.
            if not self.items or self.items[-1].appearance != appearance:
                self.ohno.logger.tile('New item!')
                self.items = [item.create(self, appearance)]
                # Since this tile might have new information,
                # we set explored to False. That way, it'll be easier for our AI
                # to know this square is interesting.
                self.explored = False
            self.ohno.logger.tile('Itemtile is now %r' % self)

        elif self == self.ohno.dungeon.curtile:
            self.ohno.logger.tile('Herotile! %r' % self)
            assert not self.has_hero
            self._walkable = True
            self.has_hero = True
            self.set_monster(None)
            if self.ohno.hero.appearance != appearance:
                # Not sure if we need this, but this seems like a good place to
                # check if we're polymorphed.
                self.ohno.hero.set_appearance(appearance)
            self.ohno.logger.tile('Herotile is now %r' % self)

        elif _tile_is_monster(appearance):
            # TODO: Might not be true if this is a stone giant standing on a
            #       boulder.
            self.ohno.logger.tile('Monstertile! %r' % self)
            self.has_hero = False
            self._walkable = True
            if (not self.monster) or self.monster.appearance != appearance:
                self.ohno.logger.tile('New monster!')
                self.set_monster(appearance)
            self.ohno.logger.tile('Monstertile is now %r' % self)

    def set_feature(self, appearance):
            self.feature = feature.create(self, appearance)
            self._walkable = (self.is_open_door() or
                                _tile_is_walkable(appearance))

    def set_monster(self, appearance):
        """Sets self.monster and updates Levle.monsters"""
        new_monster = None
        if appearance is not None:
            new_monster = monster.create(self, appearance)

        if new_monster == self.monster:
            return

        if self.monster:
            self.level.monsters.remove(self.monster)

        if new_monster:
            self.level.monsters.append(new_monster)
            # Sometimes, a monster can open a door and right on it in one turn,
            # this will confuse ohno as it will still think it's a door under
            # the monster.
            if self.feature_isa('Door') and self.feature.closed:
                self.feature = None

        self.monster = new_monster

    @property
    def appearance(self):
        """What does this tile look like right now?"""
        # This could be cached, but doing it like this works as a sanity check
        # aswell :-)
        if self.monster is not None:
            return self.monster.appearance
        elif self.has_hero:
            return self.ohno.hero.appearance
        elif self.items:
            return self.items[-1].appearance
        elif self.feature is not None:
            return self.feature.appearance
        else:
            return None

    def distance_from_hero(self):
        """
        Returns the distance from hero as calculated by ai.pathing.
        If the tile is unreachable, it will return float('inf').
        """
        assert self.ohno.ai.pathing.tick == self.ohno.tick
        return self.ohno.ai.pathing.dists[self.idx]

    def feature_isa(self, class_name):
        return self.feature_name == class_name

    def is_open_door(self):
        return (self.feature and
                self.feature.appearance.glyph in '-|' and
                self.feature.appearance.fg == 33)

    # Methods for iterating neighbors
    @queryable
    def adjacent(self):
        """Return the direct neighbors of this tile."""
        # Since ohno.dungeon.level.tiles never changes (it's even a tuple), we
        # can safely cache the result of this function the first time we run it.
        if self._adjacent is None:
            self._adjacent = []
            dirs = (
                (-1, -1), (-1, 0), (-1, 1),
                ( 1, -1), ( 1, 0), ( 1, 1),
                ( 0, -1), ( 0, 1)
            )
            x, y = self.idx % 80, self.idx / 80
            for x2, y2 in ((dir[0] + x, dir[1] + y) for dir in dirs):
                if 0 <= x2 < 80 and 0 <= y2 < 21:
                    self._adjacent.append(self.level.tiles[y2 * 80 + x2])
            self._adjacent = tuple(self._adjacent)
        assert len(self._adjacent) <= 8
        return self._adjacent

    @queryable
    def orthogonal(self):
        if self._orthogonal is None:
            self._orthogonal = tuple(n for n in self.adjacent()
                                            if abs(self.idx - n.idx) in (1, 80))
        assert len(self._orthogonal) <= 4
        return self._orthogonal

    @queryable
    def vertical(self):
        if self._vertical is None:
            self._vertical = tuple(o for o in self.orthogonal()
                                            if abs(self.idx - o.idx) == 1)
        assert len(self._vertical) <= 2
        return self._vertical

    @queryable
    def horizontal(self):
        if self._horizontal is None:
            self._horizontal = tuple(o for o in self.orthogonal()
                                            if abs(self.idx - o.idx) == 80)
        assert len(self._horizontal) <= 2
        return self._horizontal

    # Methods to make it simpler to query
    @property
    def is_wall(self):
        return (self.explored and self.feature and
                self.feature.appearance.glyph in '|- ' and
                self.feature.appearance.fg == 37)

    @property
    def is_hallway(self):
        return (self.explored and self.feature and
                self.feature.appearance.glyph == '#' and
                self.feature.appearance.fg == 37)

    @property
    def is_floor(self):
        return (self.explored and self.feature and
                self.feature.appearance.glyph == '.' and
                self.feature.appearance.fg == 37)

    @property
    def has_monster(self):
        return self.monster is not None

    @property
    def has_closed_door(self):
        return self.feature_isa('Door') and self.feature.closed

    @property
    def feature_name(self):
        return self.feature.__class__.__name__ if self.feature else 'Unknown'

    @property
    def reachable(self):
        assert self.ohno.ai.pathing.is_uptodate()
        return self.distance_from_hero() != float('inf')

    @property
    def walkable(self):
        # This might be called before the tile is initialized, so make sure we
        # consider the case where appearance is None
        return (self.appearance and
                self._walkable and
                not self.monster and
                self.appearance.glyph != '0')
<|endoftext|>


<commit_before># future
from __future__ import annotations

# stdlib
# from typing import DefaultDict as TypeDefaultDict
# from collections import Counter
# from collections import defaultdict
from typing import Any
from typing import List
from typing import Optional
from typing import Type
from typing import Union
import uuid

# third party
import numpy as np

# relative
from .. import autograd
from ....core.adp.collections import DefaultDict
from ....core.adp.collections import SerializableCounter
from ....core.common.serde.recursive import RecursiveSerde
from ...common.serde.serializable import bind_protobuf
from ..ancestors import AutogradTensorAncestor
from ..ancestors import PhiTensorAncestor
from ..passthrough import AcceptableSimpleType
from ..passthrough import PassthroughTensor
from ..passthrough import is_acceptable_simple_type

@bind_protobuf
class AutogradTensor(PassthroughTensor, PhiTensorAncestor, RecursiveSerde):

    __attr_allowlist__ = [
        "child",
        "_grad",
        "_grad_fn",
        "ops",
        "backprop_id",
        "n_backwards",
    ]

    def __init__(
        self,
        child: Union[Type[AutogradTensor], AcceptableSimpleType],
        requires_grad: bool = False,
    ) -> None:
        super().__init__(child)

        # whether to run backpropagation or not
        self.requires_grad = requires_grad

        # tensor gradient
        self._grad: DefaultDict = DefaultDict(lambda: None)

        # operation used to create this tensor (if any)
        self._grad_fn: Optional[Type[autograd.backward_ops.Op]] = None

        # list of ops which use this tensor
        self.ops: List = list()

        self.backprop_id: Optional[uuid.UUID] = None

        # self.n_backwards: Counter[uuid.UUID] = Counter()
        self.n_backwards: SerializableCounter = (
            SerializableCounter()
        )  # may have to add [uuid.UUID] for type annotation

    @property
    def grad(self) -> Optional[np.ndarray]:
        if self.backprop_id not in self._grad:
            return None
        return self._grad[self.backprop_id]

    @property
    def grad_fn(
        self,
    ) -> Optional[Type[autograd.backward_ops.Op]]:
        if not self.requires_grad:
            raise Exception("This tensor is not backpropagated")
        return self._grad_fn

    # Autograd Tensor Operations
    """ Note: Ignoring return type incompatibilities since AutogradTensorAncestor doesn't inherit from
    PassThroughTensor"""

    def __abs__(self) -> AutogradTensorAncestor:
        op = autograd.backward_ops.AbsOp()
        return op(self)

    def __add__(self, other: AutogradTensor) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.AddOp()
        return op(self, other)

    def __sub__(self, other: AutogradTensor) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.SubOp()
        return op(self, other)

    def __mul__(self, other: AutogradTensor) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.MulOp()
        return op(self, other)

    def __rmul__(self, other: AutogradTensor) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.MulOp()
        return op(self, other)

    def __truediv__(self, other: AutogradTensor) -> AutogradTensorAncestor:  # type: ignore
        if is_acceptable_simple_type(other):
            # Ignoring type annotation error because only int, floats, np.ndarrays will be parsed
            return self * (1 / other)  # type: ignore
        return NotImplemented

    def __pow__(self, other: Any) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.PowOp()
        return op(self, other)

    def __rpow__(self, other: Any) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.RPowOp()
        return op(self, other)

    def reshape(self, *shape: tuple) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.ReshapeOp()
        return op(self, *shape)

    def repeat(self, *args: int, **kwargs: int) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.RepeatOp()
        return op(self, *args, **kwargs)

    def copy(self) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.CopyOp()
        return op(self)

    def sum(self, *args: int, **kwargs: int) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.SumOp()
        return op(self, *args, **kwargs)

    def transpose(self, *dims: tuple) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.TransposeOp()
        return op(self, *dims)

    # End Autograd Tensor Operations

    def add_grad(self, grad: np.ndarray) -> None:

        # print("Adding grad:" + str(type(grad)) + " w/ backprop_id:" + str(self.backprop_id))

        if self._grad[self.backprop_id] is None:
            self._grad[self.backprop_id] = grad
        else:

            self._grad[self.backprop_id] = self._grad[self.backprop_id] + grad

    def backward(
        self,
        grad: Optional[np.ndarray] = None,
        backprop_id: Optional[uuid.UUID] = None,
    ) -> bool:

        if backprop_id is None:
            backprop_id = uuid.uuid4()

        self.n_backwards[backprop_id] += 1

        self.backprop_id = backprop_id

        if not self.grad_fn:
            return False

        if grad is None and self._grad[self.backprop_id] is None:
            # in case if this is last loss tensor
            grad = np.ones(self.shape)
            # grad = self.__class__(grad, requires_grad=False)
            # this more or less ensures it has the right tensor chain
            # grad = (self * 0) + 1

        elif self.grad is not None:
            grad = self._grad[self.backprop_id]

        if not self.requires_grad:
            raise Exception("This tensor is not backpropagated")

        # if all gradients are accounted for - backprop
        if self.n_backwards[backprop_id] >= len(self.ops):

            self.grad_fn.backward(grad, backprop_id=backprop_id)  # type: ignore

        # if some gradietns appear to be missing - parse forward in
        # the graph to double check
        else:

            # investigate whether any of the missing ops are actually
            # going to get used.
            found_id = False

            n_direct_ops = 0
            for op in self.ops:
                if op.backprop_id is not None and op.backprop_id == backprop_id:
                    n_direct_ops += 1

            # if the number of operations we know will be backpropagating gradients to us
            # exceeds the number of times we've been backpropgated into - then we know
            # we need to wait.
            if n_direct_ops > self.n_backwards[backprop_id]:
                found_id = True

            else:

                for op in self.ops:
                    if op.backprop_id is None:
                        if op.out.find_backprop_id(self.backprop_id):
                            found_id = True
                            break

            if found_id:
                "do nothing - we're going to get another gradient"
            else:
                # backprop anyway - we've got all the grads we're gonna get
                self.grad_fn.backward(grad, backprop_id=backprop_id)  # type: ignore

        return True

    def find_backprop_id(self, backprop_id: Optional[uuid.UUID]) -> bool:
        found_id = False

        for op in self.ops:
            if op.backprop_id is not None and op.backprop_id == backprop_id:
                return True

            if op.out.find_backprop_id(self.backprop_id):
                found_id = True
                break

        return found_id
<commit_msg>ADD requires_grad at __attr_allowlist__ to be serializable<commit_after># future
from __future__ import annotations

# stdlib
# from typing import DefaultDict as TypeDefaultDict
# from collections import Counter
# from collections import defaultdict
from typing import Any
from typing import List
from typing import Optional
from typing import Type
from typing import Union
import uuid

# third party
import numpy as np

# relative
from .. import autograd
from ....core.adp.collections import DefaultDict
from ....core.adp.collections import SerializableCounter
from ....core.common.serde.recursive import RecursiveSerde
from ...common.serde.serializable import bind_protobuf
from ..ancestors import AutogradTensorAncestor
from ..ancestors import PhiTensorAncestor
from ..passthrough import AcceptableSimpleType
from ..passthrough import PassthroughTensor
from ..passthrough import is_acceptable_simple_type

@bind_protobuf
class AutogradTensor(PassthroughTensor, PhiTensorAncestor, RecursiveSerde):

    __attr_allowlist__ = [
        "child",
        "requires_grad",
        "_grad",
        "_grad_fn",
        "ops",
        "backprop_id",
        "n_backwards",
    ]

    def __init__(
        self,
        child: Union[Type[AutogradTensor], AcceptableSimpleType],
        requires_grad: bool = False,
    ) -> None:
        super().__init__(child)

        # whether to run backpropagation or not
        self.requires_grad = requires_grad

        # tensor gradient
        self._grad: DefaultDict = DefaultDict(lambda: None)

        # operation used to create this tensor (if any)
        self._grad_fn: Optional[Type[autograd.backward_ops.Op]] = None

        # list of ops which use this tensor
        self.ops: List = list()

        self.backprop_id: Optional[uuid.UUID] = None

        # self.n_backwards: Counter[uuid.UUID] = Counter()
        self.n_backwards: SerializableCounter = (
            SerializableCounter()
        )  # may have to add [uuid.UUID] for type annotation

    @property
    def grad(self) -> Optional[np.ndarray]:
        if self.backprop_id not in self._grad:
            return None
        return self._grad[self.backprop_id]

    @property
    def grad_fn(
        self,
    ) -> Optional[Type[autograd.backward_ops.Op]]:
        if not self.requires_grad:
            raise Exception("This tensor is not backpropagated")
        return self._grad_fn

    # Autograd Tensor Operations
    """ Note: Ignoring return type incompatibilities since AutogradTensorAncestor doesn't inherit from
    PassThroughTensor"""

    def __abs__(self) -> AutogradTensorAncestor:
        op = autograd.backward_ops.AbsOp()
        return op(self)

    def __add__(self, other: AutogradTensor) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.AddOp()
        return op(self, other)

    def __sub__(self, other: AutogradTensor) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.SubOp()
        return op(self, other)

    def __mul__(self, other: AutogradTensor) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.MulOp()
        return op(self, other)

    def __rmul__(self, other: AutogradTensor) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.MulOp()
        return op(self, other)

    def __truediv__(self, other: AutogradTensor) -> AutogradTensorAncestor:  # type: ignore
        if is_acceptable_simple_type(other):
            # Ignoring type annotation error because only int, floats, np.ndarrays will be parsed
            return self * (1 / other)  # type: ignore
        return NotImplemented

    def __pow__(self, other: Any) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.PowOp()
        return op(self, other)

    def __rpow__(self, other: Any) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.RPowOp()
        return op(self, other)

    def reshape(self, *shape: tuple) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.ReshapeOp()
        return op(self, *shape)

    def repeat(self, *args: int, **kwargs: int) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.RepeatOp()
        return op(self, *args, **kwargs)

    def copy(self) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.CopyOp()
        return op(self)

    def sum(self, *args: int, **kwargs: int) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.SumOp()
        return op(self, *args, **kwargs)

    def transpose(self, *dims: tuple) -> AutogradTensorAncestor:  # type: ignore
        op = autograd.backward_ops.TransposeOp()
        return op(self, *dims)

    # End Autograd Tensor Operations

    def add_grad(self, grad: np.ndarray) -> None:

        # print("Adding grad:" + str(type(grad)) + " w/ backprop_id:" + str(self.backprop_id))

        if self._grad[self.backprop_id] is None:
            self._grad[self.backprop_id] = grad
        else:

            self._grad[self.backprop_id] = self._grad[self.backprop_id] + grad

    def backward(
        self,
        grad: Optional[np.ndarray] = None,
        backprop_id: Optional[uuid.UUID] = None,
    ) -> bool:

        if backprop_id is None:
            backprop_id = uuid.uuid4()

        self.n_backwards[backprop_id] += 1

        self.backprop_id = backprop_id

        if not self.grad_fn:
            return False

        if grad is None and self._grad[self.backprop_id] is None:
            # in case if this is last loss tensor
            grad = np.ones(self.shape)
            # grad = self.__class__(grad, requires_grad=False)
            # this more or less ensures it has the right tensor chain
            # grad = (self * 0) + 1

        elif self.grad is not None:
            grad = self._grad[self.backprop_id]

        if not self.requires_grad:
            raise Exception("This tensor is not backpropagated")

        # if all gradients are accounted for - backprop
        if self.n_backwards[backprop_id] >= len(self.ops):

            self.grad_fn.backward(grad, backprop_id=backprop_id)  # type: ignore

        # if some gradietns appear to be missing - parse forward in
        # the graph to double check
        else:

            # investigate whether any of the missing ops are actually
            # going to get used.
            found_id = False

            n_direct_ops = 0
            for op in self.ops:
                if op.backprop_id is not None and op.backprop_id == backprop_id:
                    n_direct_ops += 1

            # if the number of operations we know will be backpropagating gradients to us
            # exceeds the number of times we've been backpropgated into - then we know
            # we need to wait.
            if n_direct_ops > self.n_backwards[backprop_id]:
                found_id = True

            else:

                for op in self.ops:
                    if op.backprop_id is None:
                        if op.out.find_backprop_id(self.backprop_id):
                            found_id = True
                            break

            if found_id:
                "do nothing - we're going to get another gradient"
            else:
                # backprop anyway - we've got all the grads we're gonna get
                self.grad_fn.backward(grad, backprop_id=backprop_id)  # type: ignore

        return True

    def find_backprop_id(self, backprop_id: Optional[uuid.UUID]) -> bool:
        found_id = False

        for op in self.ops:
            if op.backprop_id is not None and op.backprop_id == backprop_id:
                return True

            if op.out.find_backprop_id(self.backprop_id):
                found_id = True
                break

        return found_id
<|endoftext|>