<commit_before>import os, subprocess
import argparse
import tempfile
import shutil
import itertools

from pathlib import Path
from . import build, minstall, dependencies
from .mesonlib import MesonException, RealPathAction, is_windows, setup_vsenv, OptionKey, quote_arg
from . import mlog

import typing as T
if T.TYPE_CHECKING:
    from .backends import InstallData

def add_arguments(parser: argparse.ArgumentParser) -> None:
    parser.add_argument('-C', dest='wd', action=RealPathAction,
                        help='Directory to cd into before running')
    parser.add_argument('--dump', action='store_true',
                        help='Only print required environment (Since 0.62.0)')
    parser.add_argument('command', nargs=argparse.REMAINDER,
                        help='Command to run in developer environment (default: interactive shell)')

def get_windows_shell() -> str:
    mesonbuild = Path(__file__).parent
    script = mesonbuild / 'scripts' / 'cmd_or_ps.ps1'
    command = ['powershell.exe', '-noprofile', '-executionpolicy', 'bypass', '-file', str(script)]
    result = subprocess.check_output(command)
    return result.decode().strip()

def get_env(b: build.Build, build_dir: str) -> T.Tuple[T.Dict[str, str], T.Set[str]]:
    extra_env = build.EnvironmentVariables()
    extra_env.set('MESON_DEVENV', ['1'])
    extra_env.set('MESON_PROJECT_NAME', [b.project_name])

    meson_uninstalled = Path(build_dir) / 'meson-uninstalled'
    if meson_uninstalled.is_dir():
        extra_env.prepend('PKG_CONFIG_PATH', [str(meson_uninstalled)])

    env = os.environ.copy()
    varnames = set()
    for i in itertools.chain(b.devenv, {extra_env}):
        env = i.get_env(env)
        varnames |= i.get_names()

    return env, varnames

def bash_completion_files(b: build.Build, install_data: 'InstallData') -> T.List[str]:
    result = []
    dep = dependencies.PkgConfigDependency('bash-completion', b.environment,
                                           {'silent': True, 'version': '>=2.10'})
    if dep.found():
        prefix = b.environment.coredata.get_option(OptionKey('prefix'))
        assert isinstance(prefix, str), 'for mypy'
        datadir = b.environment.coredata.get_option(OptionKey('datadir'))
        assert isinstance(datadir, str), 'for mypy'
        datadir_abs = os.path.join(prefix, datadir)
        completionsdir = dep.get_variable(pkgconfig='completionsdir', pkgconfig_define=['datadir', datadir_abs])
        assert isinstance(completionsdir, str), 'for mypy'
        completionsdir_path = Path(completionsdir)
        for f in install_data.data:
            if completionsdir_path in Path(f.install_path).parents:
                result.append(f.path)
    return result

def add_gdb_auto_load(autoload_path: Path, gdb_helper: str, fname: Path) -> None:
    # Copy or symlink the GDB helper into our private directory tree
    destdir = autoload_path / fname.parent
    destdir.mkdir(parents=True, exist_ok=True)
    try:
        if is_windows():
            shutil.copy(gdb_helper, str(destdir / os.path.basename(gdb_helper)))
        else:
            os.symlink(gdb_helper, str(destdir / os.path.basename(gdb_helper)))
    except (FileExistsError, shutil.SameFileError):
        pass

def write_gdb_script(privatedir: Path, install_data: 'InstallData') -> None:
    if not shutil.which('gdb'):
        return
    bdir = privatedir.parent
    autoload_basedir = privatedir / 'gdb-auto-load'
    autoload_path = Path(autoload_basedir, *bdir.parts[1:])
    have_gdb_helpers = False
    for d in install_data.data:
        if d.path.endswith('-gdb.py') or d.path.endswith('-gdb.gdb') or d.path.endswith('-gdb.scm'):
            # This GDB helper is made for a specific shared library, search if
            # we have it in our builddir.
            libname = Path(d.path).name.rsplit('-', 1)[0]
            for t in install_data.targets:
                path = Path(t.fname)
                if path.name == libname:
                    add_gdb_auto_load(autoload_path, d.path, path)
                    have_gdb_helpers = True
    if have_gdb_helpers:
        gdbinit_line = f'add-auto-load-scripts-directory {autoload_basedir}\n'
        gdbinit_path = bdir / '.gdbinit'
        first_time = False
        try:
            with gdbinit_path.open('r+', encoding='utf-8') as f:
                if gdbinit_line not in f.readlines():
                    f.write(gdbinit_line)
                    first_time = True
        except FileNotFoundError:
            gdbinit_path.write_text(gdbinit_line, encoding='utf-8')
            first_time = True
        if first_time:
            mlog.log('Meson detected GDB helpers and added config in', mlog.bold(str(gdbinit_path)))

def run(options: argparse.Namespace) -> int:
    privatedir = Path(options.wd) / 'meson-private'
    buildfile = privatedir / 'build.dat'
    if not buildfile.is_file():
        raise MesonException(f'Directory {options.wd!r} does not seem to be a Meson build directory.')
    b = build.load(options.wd)

    devenv, varnames = get_env(b, options.wd)
    if options.dump:
        if options.command:
            raise MesonException('--dump option does not allow running other command.')
        for name in varnames:
            print(f'{name}={quote_arg(devenv[name])}')
            print(f'export {name}')
        return 0

    install_data = minstall.load_install_data(str(privatedir / 'install.dat'))
    write_gdb_script(privatedir, install_data)

    setup_vsenv(b.need_vsenv)

    args = options.command
    if not args:
        prompt_prefix = f'[{b.project_name}]'
        shell_env = os.environ.get("SHELL")
        # Prefer $SHELL in a MSYS2 bash despite it being Windows
        if shell_env and os.path.exists(shell_env):
            args = [shell_env]
        elif is_windows():
            shell = get_windows_shell()
            if shell == 'powershell.exe':
                args = ['powershell.exe']
                args += ['-NoLogo', '-NoExit']
                prompt = f'function global:prompt {{  "{prompt_prefix} PS " + $PWD + "> "}}'
                args += ['-Command', prompt]
            else:
                args = [os.environ.get("COMSPEC", r"C:\WINDOWS\system32\cmd.exe")]
                args += ['/k', f'prompt {prompt_prefix} $P$G']
        else:
            args = [os.environ.get("SHELL", os.path.realpath("/bin/sh"))]
        if "bash" in args[0]:
            # Let the GC remove the tmp file
            tmprc = tempfile.NamedTemporaryFile(mode='w')
            tmprc.write('[ -e ~/.bashrc ] && . ~/.bashrc\n')
            if not os.environ.get("MESON_DISABLE_PS1_OVERRIDE"):
                tmprc.write(f'export PS1="{prompt_prefix} $PS1"\n')
            for f in bash_completion_files(b, install_data):
                tmprc.write(f'. "{f}"\n')
            tmprc.flush()
            args.append("--rcfile")
            args.append(tmprc.name)

    try:
        return subprocess.call(args, close_fds=False,
                               env=devenv,
                               cwd=options.wd)
    except subprocess.CalledProcessError as e:
        return e.returncode
<commit_msg>devenv: Catch FileNotFoundError<commit_after>import os, subprocess
import argparse
import tempfile
import shutil
import itertools

from pathlib import Path
from . import build, minstall, dependencies
from .mesonlib import MesonException, RealPathAction, is_windows, setup_vsenv, OptionKey, quote_arg
from . import mlog

import typing as T
if T.TYPE_CHECKING:
    from .backends import InstallData

def add_arguments(parser: argparse.ArgumentParser) -> None:
    parser.add_argument('-C', dest='wd', action=RealPathAction,
                        help='Directory to cd into before running')
    parser.add_argument('--dump', action='store_true',
                        help='Only print required environment (Since 0.62.0)')
    parser.add_argument('command', nargs=argparse.REMAINDER,
                        help='Command to run in developer environment (default: interactive shell)')

def get_windows_shell() -> str:
    mesonbuild = Path(__file__).parent
    script = mesonbuild / 'scripts' / 'cmd_or_ps.ps1'
    command = ['powershell.exe', '-noprofile', '-executionpolicy', 'bypass', '-file', str(script)]
    result = subprocess.check_output(command)
    return result.decode().strip()

def get_env(b: build.Build, build_dir: str) -> T.Tuple[T.Dict[str, str], T.Set[str]]:
    extra_env = build.EnvironmentVariables()
    extra_env.set('MESON_DEVENV', ['1'])
    extra_env.set('MESON_PROJECT_NAME', [b.project_name])

    meson_uninstalled = Path(build_dir) / 'meson-uninstalled'
    if meson_uninstalled.is_dir():
        extra_env.prepend('PKG_CONFIG_PATH', [str(meson_uninstalled)])

    env = os.environ.copy()
    varnames = set()
    for i in itertools.chain(b.devenv, {extra_env}):
        env = i.get_env(env)
        varnames |= i.get_names()

    return env, varnames

def bash_completion_files(b: build.Build, install_data: 'InstallData') -> T.List[str]:
    result = []
    dep = dependencies.PkgConfigDependency('bash-completion', b.environment,
                                           {'silent': True, 'version': '>=2.10'})
    if dep.found():
        prefix = b.environment.coredata.get_option(OptionKey('prefix'))
        assert isinstance(prefix, str), 'for mypy'
        datadir = b.environment.coredata.get_option(OptionKey('datadir'))
        assert isinstance(datadir, str), 'for mypy'
        datadir_abs = os.path.join(prefix, datadir)
        completionsdir = dep.get_variable(pkgconfig='completionsdir', pkgconfig_define=['datadir', datadir_abs])
        assert isinstance(completionsdir, str), 'for mypy'
        completionsdir_path = Path(completionsdir)
        for f in install_data.data:
            if completionsdir_path in Path(f.install_path).parents:
                result.append(f.path)
    return result

def add_gdb_auto_load(autoload_path: Path, gdb_helper: str, fname: Path) -> None:
    # Copy or symlink the GDB helper into our private directory tree
    destdir = autoload_path / fname.parent
    destdir.mkdir(parents=True, exist_ok=True)
    try:
        if is_windows():
            shutil.copy(gdb_helper, str(destdir / os.path.basename(gdb_helper)))
        else:
            os.symlink(gdb_helper, str(destdir / os.path.basename(gdb_helper)))
    except (FileExistsError, shutil.SameFileError):
        pass

def write_gdb_script(privatedir: Path, install_data: 'InstallData') -> None:
    if not shutil.which('gdb'):
        return
    bdir = privatedir.parent
    autoload_basedir = privatedir / 'gdb-auto-load'
    autoload_path = Path(autoload_basedir, *bdir.parts[1:])
    have_gdb_helpers = False
    for d in install_data.data:
        if d.path.endswith('-gdb.py') or d.path.endswith('-gdb.gdb') or d.path.endswith('-gdb.scm'):
            # This GDB helper is made for a specific shared library, search if
            # we have it in our builddir.
            libname = Path(d.path).name.rsplit('-', 1)[0]
            for t in install_data.targets:
                path = Path(t.fname)
                if path.name == libname:
                    add_gdb_auto_load(autoload_path, d.path, path)
                    have_gdb_helpers = True
    if have_gdb_helpers:
        gdbinit_line = f'add-auto-load-scripts-directory {autoload_basedir}\n'
        gdbinit_path = bdir / '.gdbinit'
        first_time = False
        try:
            with gdbinit_path.open('r+', encoding='utf-8') as f:
                if gdbinit_line not in f.readlines():
                    f.write(gdbinit_line)
                    first_time = True
        except FileNotFoundError:
            gdbinit_path.write_text(gdbinit_line, encoding='utf-8')
            first_time = True
        if first_time:
            mlog.log('Meson detected GDB helpers and added config in', mlog.bold(str(gdbinit_path)))

def run(options: argparse.Namespace) -> int:
    privatedir = Path(options.wd) / 'meson-private'
    buildfile = privatedir / 'build.dat'
    if not buildfile.is_file():
        raise MesonException(f'Directory {options.wd!r} does not seem to be a Meson build directory.')
    b = build.load(options.wd)

    devenv, varnames = get_env(b, options.wd)
    if options.dump:
        if options.command:
            raise MesonException('--dump option does not allow running other command.')
        for name in varnames:
            print(f'{name}={quote_arg(devenv[name])}')
            print(f'export {name}')
        return 0

    install_data = minstall.load_install_data(str(privatedir / 'install.dat'))
    write_gdb_script(privatedir, install_data)

    setup_vsenv(b.need_vsenv)

    args = options.command
    if not args:
        prompt_prefix = f'[{b.project_name}]'
        shell_env = os.environ.get("SHELL")
        # Prefer $SHELL in a MSYS2 bash despite it being Windows
        if shell_env and os.path.exists(shell_env):
            args = [shell_env]
        elif is_windows():
            shell = get_windows_shell()
            if shell == 'powershell.exe':
                args = ['powershell.exe']
                args += ['-NoLogo', '-NoExit']
                prompt = f'function global:prompt {{  "{prompt_prefix} PS " + $PWD + "> "}}'
                args += ['-Command', prompt]
            else:
                args = [os.environ.get("COMSPEC", r"C:\WINDOWS\system32\cmd.exe")]
                args += ['/k', f'prompt {prompt_prefix} $P$G']
        else:
            args = [os.environ.get("SHELL", os.path.realpath("/bin/sh"))]
        if "bash" in args[0]:
            # Let the GC remove the tmp file
            tmprc = tempfile.NamedTemporaryFile(mode='w')
            tmprc.write('[ -e ~/.bashrc ] && . ~/.bashrc\n')
            if not os.environ.get("MESON_DISABLE_PS1_OVERRIDE"):
                tmprc.write(f'export PS1="{prompt_prefix} $PS1"\n')
            for f in bash_completion_files(b, install_data):
                tmprc.write(f'. "{f}"\n')
            tmprc.flush()
            args.append("--rcfile")
            args.append(tmprc.name)

    try:
        return subprocess.call(args, close_fds=False,
                               env=devenv,
                               cwd=options.wd)
    except subprocess.CalledProcessError as e:
        return e.returncode
    except FileNotFoundError:
        raise MesonException(f'Command not found: {args[0]}')
<|endoftext|>


<commit_before>#
# Sheared -- non-blocking network programming library for Python
# Copyright (C) 2003  Sune Kirkeby <sune@mel.interspace.dk>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

import stackless

class Semaphore:
    def __init__(self):
        self.channel = stackless.channel()
        self.taken = 0
        self.waiting = 0

    def grab(self):
        if self.taken:
            self.waiting = self.waiting + 1
            self.channel.receive()
            self.waiting = self.waiting - 1
        else:
            self.taken = 1

    def release(self):
        if self.waiting:
            self.channel.send()
        else:
            self.taken = 0
<commit_msg>Fix channel.send bug in semaphore release.<commit_after>#
# Sheared -- non-blocking network programming library for Python
# Copyright (C) 2003  Sune Kirkeby <sune@mel.interspace.dk>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

import stackless

class Semaphore:
    def __init__(self):
        self.channel = stackless.channel()
        self.taken = 0
        self.waiting = 0

    def grab(self):
        if self.taken:
            self.waiting = self.waiting + 1
            self.channel.receive()
            self.waiting = self.waiting - 1
        else:
            self.taken = 1

    def release(self):
        if self.waiting:
            self.channel.send(None)
        else:
            self.taken = 0
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-

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

"""
Classes to represent division of levels
"""

import logging

class Section():
    """
    Class representing a single section in a level
    """
    def __init__(self, corner1, corner2, level, random_generator):
        """
        Default constructor

        :param corner1: Coordinates of first corner
        :type corner1: (integer, integer)
        :param corner2: Coordinates of the second corner
        :type corner2: (integer, integer)
        :param level: level where section is linked
        :type level: Level
        :param random_generator: random number generator
        :type random_generator: Random

        .. note:: Coordinates are given relative to level origo
        """
        super(Section, self).__init__()

        self.__corners = []
        self.__corners.append(corner1)
        self.__corners.append(corner2)
        self.level = level

        self.__connections = []
        self.__room_connections = []
        self.__neighbours = []
        self.random_generator = random_generator
        self.logger = logging.getLogger('pyherc.generators.level.partitioners.section.Section') #pylint: disable=C0301

    def __get_corners(self):
        """
        Get corners of this section

        :returns: corners of the section
        :rtype: [(integer, integer), (integer, integer)]
        """
        return self.__corners

    def __set_corners(self, corners):
        """
        Set corners of this section

        :param corners: Corners to set
        :type corners: [(integer, integer), (integer, integer)]
        """
        self.__corners = corners

    def __get_connections(self):
        """
        List of connections this section has

        :returns: connections of section
        :rtype: [Connection]
        """
        return self.__connections

    def __get_room_connections(self):
        """
        List of connections leading to the room

        :returns: connections
        :rtype: [Connection]
        """
        return self.__room_connections

    def __get_neighbours(self):
        """
        List of sections next to this one

        :returns: sections next to this one
        :rtype: [Section]
        """
        return self.__neighbours

    def __get_connected(self):
        """
        Is this section connected to a neighbour

        :returns: True if connected, otherwise False
        :rtype: Boolean
        """
        return len(self.__connections) > 0

    def __get_left_edge(self):
        """
        Get leftmost point of the section

        :returns: leftmost point of the section
        :rtype: (integer, integer)
        """
        point1 = self.__corners[0][0]
        point2 = self.__corners[1][0]

        if point1 < point2:
            return point1
        else:
            return point2

    def __get_width(self):
        """
        Get width of the section

        :returns: width of the section
        :rtype: integer
        """
        return abs(self.__corners[0][0] - self.__corners[1][0])

    def __get_top_edge(self):
        """
        Get top edge of the section

        :returns: highest point of the section
        :rtype: (integer, integer)
        """
        point1 = self.__corners[0][1]
        point2 = self.__corners[1][1]

        if point1 < point2:
            return point1
        else:
            return point2

    def __get_height(self):
        """
        Get height of the section

        :returns: height of the section
        :rtype: integer
        """
        return abs(self.__corners[0][1] - self.__corners[1][1])

    corners = property(__get_corners, __set_corners)
    """Corners of this Section.""" #pylint: disable=W0105

    connections = property(__get_connections)
    """Readonly property to access connections of the section""" #pylint: disable=W0105

    room_connections = property(__get_room_connections)
    """Readonly property to access connections to the room""" #pylint: disable=W0105

    neighbours = property(__get_neighbours)
    """Readonly property to access neighbours of the section""" #pylint: disable=W0105

    connected = property(__get_connected)
    """Readonly property for connection status of the section

        :returns: True if section is connected, otherwise False
        :rtype: Boolean""" #pylint: disable=W0105

    left_edge = property(__get_left_edge)
    """Readonly property to find leftmost point of the section""" #pylint: disable=W0105

    width = property(__get_width)
    """Readonly property to calculate width of the section""" #pylint: disable=W0105

    top_edge = property(__get_top_edge)
    """Readonly property to find topmost point of the section""" #pylint: disable=W0105

    height = property(__get_height)
    """Readonly property to find height of the section""" #pylint: disable=W0105

    def connect_to(self, section):
        """
        Connect this Section to another

        :param section: section to connect to
        :type section: Section
        """
        my_side_of_border = self.get_common_border(section)
        my_side = self.random_generator.choice(my_side_of_border)
        my_connection = Connection(connection = section,
                                   location = (my_side[0], my_side[1]),
                                   direction = my_side[2],
                                   section = self)
        self.connections.append(my_connection)

        other_side = section.get_opposing_point(my_side)
        other_connection = Connection(connection = self,
                                   location = (other_side[0], other_side[1]),
                                   direction = other_side[2],
                                   section = section)
        section.connections.append(other_connection)

    def unconnected_neighbours(self):
        """
        Get list of unconnected neighbours

        :returns: unconnected neighbours
        :rtype: [Section]
        """
        return [x for x in self.neighbours
                        if x.connected == False]

    def has_unconnected_neighbours(self):
        """
        Check if any of this Sections neighbours is unconnected

        :returns: True if unconnected neighbour is found, otherwise false
        :rtype: Boolean
        """
        return len(self.unconnected_neighbours()) > 0

    def get_border(self):
        """
        Get list of locations, defining borders of this Section

        :returns: List of (loc_x, loc_y, direction) defining borders
        :rtype: [(integer, integer, string)

        .. note:: coordinates are given relative to level origo
        """
        border = []

        assert(len(self.__corners) == 2)
        assert(len(self.__corners[0]) == 2)
        assert(len(self.__corners[1]) == 2)

        for loc_x in range(self.__corners[0][0] + 1, self.__corners[1][0]):
            border.append((loc_x, self.__corners[0][1], "down"))
            border.append((loc_x, self.__corners[1][1], "up"))

        for loc_y in range(self.__corners[0][1] + 1, self.__corners[1][1]):
            border.append((self.__corners[0][0], loc_y, "right"))
            border.append((self.__corners[1][0], loc_y, "left"))

        return border

    def get_common_border(self, another_section):
        """
        Get list of locations that define common border between two Sections
        Border is placed on the edge of this Section

        :returns: List of (loc_x, loc_y, direction) defining common border
        :rtype: [(integer, integer, string)]

        .. note:: Coordinates are given relative to level origo
        """
        my_border = self.get_border()
        other_border = another_section.get_border()
        common_border = []

        for loc_1 in my_border:
            for loc_2 in other_border:
                if (loc_1[0] - loc_2[0])**2 + (loc_1[1] - loc_2[1])**2 == 1:
                    common_border.append(loc_1)

        return common_border

    def get_opposing_point(self, location):
        """
        Calculate which of this Section's points corresponds to the point given
        on the other side of the common border

        :param location: (loc_x, loc_y) defining point on the other side
        :type location: (integer, integer)
        :returns: (loc_x, loc_y) if corresponding point is found, False otherwise
        :rtype: (integer, integer) or Boolean

        .. note:: Coordinates are given relative to level origo
        """
        my_side = None
        my_border = self.get_border()

        for loc_1 in my_border:
            if (loc_1[0] - location[0])**2 + (loc_1[1] - location[1])**2 == 1:
                my_side = loc_1

        return my_side

    def add_room_connection(self, location, direction):
        """
        Adds connection to the room

        Room connections are used to connect rooms to edge of Sections

        :param location: location where to add the Connection
        :type location: (integer, integer)
        :direction: direction where this connections leads
        :type direction: string

        .. note:: Coordinates are given relative to section origo
        """
        self.__room_connections.append(
                        Connection(connection = None,
                                   location = location,
                                   direction = direction,
                                   section = self))

    def set_floor(self, location, tile, location_type):
        """
        Set floor at given location

        :param location: location to set the tile
        :type location: (integer, integer)
        :param tile: ID of the tile to use
        :type tile: integer
        :param location_type: type of location, None to not change
        :type location_type: string

        .. note:: Coordinates are given relative to section origo
        """
        x_loc = self.__get_left_edge() + location[0]
        y_loc = self.__get_top_edge() + location[1]

        self.level.floor[x_loc][y_loc] = tile
        if location_type != None:
            self.level.set_location_type((x_loc, y_loc), location_type)

    def get_floor(self, location):
        """
        Get floor tile in given location

        :param location: location to check
        :type location: (int, int)
        :returns: floor tile
        :rtype: int

        .. versionadded:: 0.8
        """
        x_loc = self.__get_left_edge() + location[0]
        y_loc = self.__get_top_edge() + location[1]

        return self.level.floor[x_loc][y_loc]

    def set_wall(self, location, tile, location_type):
        """
        Set wall at given location

        :param location: location to set the tile
        :type location: (integer, integer)
        :param tile: ID of the tile to use
        :type tile: integer
        :param location_type: type of location, None to not change
        :type location_type: string

        .. note:: Coordinates are given relative to section origo
        """
        x_loc = self.__get_left_edge() + location[0]
        y_loc = self.__get_top_edge() + location[1]

        self.level.walls[x_loc][y_loc] = tile
        if location_type != None:
            self.level.set_location_type(location, location_type)

    def set_location_type(self, location, location_type):
        """
        Set type of location in level

        :param location: location to set
        :type location: (int, int)
        :param location_type: type of location
        :type location_type: string

        .. versionadded:: 0.8
        """
        x_loc = self.__get_left_edge() + location[0]
        y_loc = self.__get_top_edge() + location[1]

        self.level.set_location_type((x_loc, y_loc), location_type)

    def get_wall(self, location):
        """
        Get wall at given location

        :param location: location to check
        :type location: (int, int)
        :returns: wall at given location
        :rtype: int

        .. versionadded:: 0.8
        """
        x_loc = self.__get_left_edge() + location[0]
        y_loc = self.__get_top_edge() + location[1]

        return self.level.walls[x_loc][y_loc]

    def find_room_connection(self, section_connection):
        """
        Find room connection that matches to given section connection

        :param section_connection: connection at the edge of section
        :type section_connection: Section
        :returns: matching room connection
        :rtype: Connection
        """
        wanted = None
        if section_connection.direction == "left":
            wanted = "right"
        elif section_connection.direction == "right":
            wanted = "left"
        elif section_connection.direction == "up":
            wanted = "down"
        else:
            wanted = "up"

        possible_connections = [x for x in self.room_connections
                                if x.direction == wanted]

        connection = self.random_generator.choice(possible_connections)

        return connection

class Connection():
    """
    Connection between Sections or between Section and room
    """
    def __init__(self, connection, location, direction, section):
        """
        Default constructor

        :param connection: Connection on another Section,
                           if connecting between sections
        :type connection: Connection
        :param location: location of this connection
        :type location: (integer, integer)
        :param direction: direction where corridor should start from here
        :type direction: string
        :param section: section where connection is located
        :type section: Section
        """
        super(Connection, self).__init__()

        self.connection = connection
        self.location = location
        self.direction = direction
        self.section = section

    def translate_to_section(self):
        """
        Create a new Connection with coordinates translated to section

        :returns: new connection
        :rtype: Connection
        """
        new_location = (self.location[0] - self.section.left_edge,
                        self.location[1] - self.section.top_edge)

        new_connection = Connection(self.connection,
                                    new_location,
                                    self.direction,
                                    self.section)

        return new_connection

<commit_msg>code clean up<commit_after># -*- coding: utf-8 -*-

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

"""
Classes to represent division of levels
"""

import logging

class Section():
    """
    Class representing a single section in a level
    """
    def __init__(self, corner1, corner2, level, random_generator):
        """
        Default constructor

        :param corner1: Coordinates of first corner
        :type corner1: (integer, integer)
        :param corner2: Coordinates of the second corner
        :type corner2: (integer, integer)
        :param level: level where section is linked
        :type level: Level
        :param random_generator: random number generator
        :type random_generator: Random

        .. note:: Coordinates are given relative to level origo
        """
        super().__init__()

        self.__corners = []
        self.__corners.append(corner1)
        self.__corners.append(corner2)
        self.level = level

        self.__connections = []
        self.__room_connections = []
        self.__neighbours = []
        self.random_generator = random_generator
        self.logger = logging.getLogger('pyherc.generators.level.partitioners.section.Section')  # noqa

    def __get_corners(self):
        """
        Get corners of this section

        :returns: corners of the section
        :rtype: [(integer, integer), (integer, integer)]
        """
        return self.__corners

    def __set_corners(self, corners):
        """
        Set corners of this section

        :param corners: Corners to set
        :type corners: [(integer, integer), (integer, integer)]
        """
        self.__corners = corners

    def __get_connections(self):
        """
        List of connections this section has

        :returns: connections of section
        :rtype: [Connection]
        """
        return self.__connections

    def __get_room_connections(self):
        """
        List of connections leading to the room

        :returns: connections
        :rtype: [Connection]
        """
        return self.__room_connections

    def __get_neighbours(self):
        """
        List of sections next to this one

        :returns: sections next to this one
        :rtype: [Section]
        """
        return self.__neighbours

    def __get_connected(self):
        """
        Is this section connected to a neighbour

        :returns: True if connected, otherwise False
        :rtype: Boolean
        """
        return len(self.__connections) > 0

    def __get_left_edge(self):
        """
        Get leftmost point of the section

        :returns: leftmost point of the section
        :rtype: (integer, integer)
        """
        point1 = self.__corners[0][0]
        point2 = self.__corners[1][0]

        if point1 < point2:
            return point1
        else:
            return point2

    def __get_width(self):
        """
        Get width of the section

        :returns: width of the section
        :rtype: integer
        """
        return abs(self.__corners[0][0] - self.__corners[1][0])

    def __get_top_edge(self):
        """
        Get top edge of the section

        :returns: highest point of the section
        :rtype: (integer, integer)
        """
        point1 = self.__corners[0][1]
        point2 = self.__corners[1][1]

        if point1 < point2:
            return point1
        else:
            return point2

    def __get_height(self):
        """
        Get height of the section

        :returns: height of the section
        :rtype: integer
        """
        return abs(self.__corners[0][1] - self.__corners[1][1])

    corners = property(__get_corners, __set_corners)
    """Corners of this Section."""

    connections = property(__get_connections)
    """Readonly property to access connections of the section"""

    room_connections = property(__get_room_connections)
    """Readonly property to access connections to the room"""

    neighbours = property(__get_neighbours)
    """Readonly property to access neighbours of the section"""

    connected = property(__get_connected)
    """Readonly property for connection status of the section

        :returns: True if section is connected, otherwise False
        :rtype: Boolean"""

    left_edge = property(__get_left_edge)
    """Readonly property to find leftmost point of the section"""

    width = property(__get_width)
    """Readonly property to calculate width of the section"""

    top_edge = property(__get_top_edge)
    """Readonly property to find topmost point of the section"""

    height = property(__get_height)
    """Readonly property to find height of the section"""

    def connect_to(self, section):
        """
        Connect this Section to another

        :param section: section to connect to
        :type section: Section
        """
        my_side_of_border = self.get_common_border(section)
        my_side = self.random_generator.choice(my_side_of_border)
        my_connection = Connection(connection=section,
                                   location=(my_side[0], my_side[1]),
                                   direction=my_side[2],
                                   section=self)
        self.connections.append(my_connection)

        other_side = section.get_opposing_point(my_side)
        other_connection = Connection(connection=self,
                                      location=(other_side[0],
                                                other_side[1]),
                                      direction=other_side[2],
                                      section=section)
        section.connections.append(other_connection)

    def unconnected_neighbours(self):
        """
        Get list of unconnected neighbours

        :returns: unconnected neighbours
        :rtype: [Section]
        """
        return [x for x in self.neighbours
                if not x.connected]

    def has_unconnected_neighbours(self):
        """
        Check if any of this Sections neighbours is unconnected

        :returns: True if unconnected neighbour is found, otherwise false
        :rtype: Boolean
        """
        return len(self.unconnected_neighbours()) > 0

    def get_border(self):
        """
        Get list of locations, defining borders of this Section

        :returns: List of (loc_x, loc_y, direction) defining borders
        :rtype: [(integer, integer, string)

        .. note:: coordinates are given relative to level origo
        """
        border = []

        assert len(self.__corners) == 2
        assert len(self.__corners[0]) == 2
        assert len(self.__corners[1]) == 2

        for loc_x in range(self.__corners[0][0] + 1, self.__corners[1][0]):
            border.append((loc_x, self.__corners[0][1], "down"))
            border.append((loc_x, self.__corners[1][1], "up"))

        for loc_y in range(self.__corners[0][1] + 1, self.__corners[1][1]):
            border.append((self.__corners[0][0], loc_y, "right"))
            border.append((self.__corners[1][0], loc_y, "left"))

        return border

    def get_common_border(self, another_section):
        """
        Get list of locations that define common border between two Sections
        Border is placed on the edge of this Section

        :returns: List of (loc_x, loc_y, direction) defining common border
        :rtype: [(integer, integer, string)]

        .. note:: Coordinates are given relative to level origo
        """
        my_border = self.get_border()
        other_border = another_section.get_border()
        common_border = []

        for loc_1 in my_border:
            for loc_2 in other_border:
                if (loc_1[0] - loc_2[0])**2 + (loc_1[1] - loc_2[1])**2 == 1:
                    common_border.append(loc_1)

        return common_border

    def get_opposing_point(self, location):
        """
        Calculate which of this Section's points corresponds to the point given
        on the other side of the common border

        :param location: (loc_x, loc_y) defining point on the other side
        :type location: (integer, integer)
        :returns: (loc_x, loc_y) if corresponding point is found
        :rtype: (integer, integer) or Boolean

        .. note:: Coordinates are given relative to level origo
        .. note:: if not match is found, False is returned
        """
        my_side = None
        my_border = self.get_border()

        for loc_1 in my_border:
            if (loc_1[0] - location[0])**2 + (loc_1[1] - location[1])**2 == 1:
                my_side = loc_1

        return my_side

    def add_room_connection(self, location, direction):
        """
        Adds connection to the room

        Room connections are used to connect rooms to edge of Sections

        :param location: location where to add the Connection
        :type location: (integer, integer)
        :direction: direction where this connections leads
        :type direction: string

        .. note:: Coordinates are given relative to section origo
        """
        self.__room_connections.append(Connection(connection=None,
                                                  location=location,
                                                  direction=direction,
                                                  section=self))

    def set_floor(self, location, tile, location_type):
        """
        Set floor at given location

        :param location: location to set the tile
        :type location: (integer, integer)
        :param tile: ID of the tile to use
        :type tile: integer
        :param location_type: type of location, None to not change
        :type location_type: string

        .. note:: Coordinates are given relative to section origo
        """
        x_loc = self.__get_left_edge() + location[0]
        y_loc = self.__get_top_edge() + location[1]

        self.level.floor[x_loc][y_loc] = tile
        if location_type is not None:
            self.level.set_location_type((x_loc, y_loc), location_type)

    def get_floor(self, location):
        """
        Get floor tile in given location

        :param location: location to check
        :type location: (int, int)
        :returns: floor tile
        :rtype: int

        .. versionadded:: 0.8
        """
        x_loc = self.__get_left_edge() + location[0]
        y_loc = self.__get_top_edge() + location[1]

        return self.level.floor[x_loc][y_loc]

    def set_wall(self, location, tile, location_type):
        """
        Set wall at given location

        :param location: location to set the tile
        :type location: (integer, integer)
        :param tile: ID of the tile to use
        :type tile: integer
        :param location_type: type of location, None to not change
        :type location_type: string

        .. note:: Coordinates are given relative to section origo
        """
        x_loc = self.__get_left_edge() + location[0]
        y_loc = self.__get_top_edge() + location[1]

        self.level.walls[x_loc][y_loc] = tile
        if location_type is not None:
            self.level.set_location_type(location, location_type)

    def set_location_type(self, location, location_type):
        """
        Set type of location in level

        :param location: location to set
        :type location: (int, int)
        :param location_type: type of location
        :type location_type: string

        .. versionadded:: 0.8
        """
        x_loc = self.__get_left_edge() + location[0]
        y_loc = self.__get_top_edge() + location[1]

        self.level.set_location_type((x_loc, y_loc), location_type)

    def get_wall(self, location):
        """
        Get wall at given location

        :param location: location to check
        :type location: (int, int)
        :returns: wall at given location
        :rtype: int

        .. versionadded:: 0.8
        """
        x_loc = self.__get_left_edge() + location[0]
        y_loc = self.__get_top_edge() + location[1]

        return self.level.walls[x_loc][y_loc]

    def find_room_connection(self, section_connection):
        """
        Find room connection that matches to given section connection

        :param section_connection: connection at the edge of section
        :type section_connection: Section
        :returns: matching room connection
        :rtype: Connection
        """
        wanted = None
        if section_connection.direction == "left":
            wanted = "right"
        elif section_connection.direction == "right":
            wanted = "left"
        elif section_connection.direction == "up":
            wanted = "down"
        else:
            wanted = "up"

        possible_connections = [x for x in self.room_connections
                                if x.direction == wanted]

        connection = self.random_generator.choice(possible_connections)

        return connection

class Connection():
    """
    Connection between Sections or between Section and room
    """
    def __init__(self, connection, location, direction, section):
        """
        Default constructor

        :param connection: Connection on another Section,
                           if connecting between sections
        :type connection: Connection
        :param location: location of this connection
        :type location: (integer, integer)
        :param direction: direction where corridor should start from here
        :type direction: string
        :param section: section where connection is located
        :type section: Section
        """
        super().__init__()

        self.connection = connection
        self.location = location
        self.direction = direction
        self.section = section

    def translate_to_section(self):
        """
        Create a new Connection with coordinates translated to section

        :returns: new connection
        :rtype: Connection
        """
        new_location = (self.location[0] - self.section.left_edge,
                        self.location[1] - self.section.top_edge)

        new_connection = Connection(self.connection,
                                    new_location,
                                    self.direction,
                                    self.section)

        return new_connection
<|endoftext|>


<commit_before>'''
@author: jnaous
'''

from django.views.generic import list_detail, create_update, simple
from models import Project
from forms import ProjectCreateForm
from django.http import HttpResponseRedirect, HttpResponseNotAllowed, Http404
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404
from expedient.clearinghouse.aggregate.models import Aggregate
import logging
from expedient.common.utils.views import generic_crud
from expedient.common.messaging.models import DatedMessage
from django.db.models import Q
from expedient.common.permissions.decorators import require_objs_permissions_for_view
from expedient.common.permissions.utils import get_queryset, get_user_from_req,\
    get_queryset_from_class
from expedient.clearinghouse.roles.models import ProjectRole,\
    ProjectRoleRequest
from expedient.common.permissions.models import ObjectPermission,\
    PermissionOwnership, Permittee
from expedient.clearinghouse.project.forms import AddMemberForm, MemberForm
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType

logger = logging.getLogger("project.views")

TEMPLATE_PATH = "project"

DEFAULT_OWNER_PERMISSIONS = [
    "can_edit_project", "can_delete_project", "can_view_project",
    "can_add_members", "can_remove_members",
    "can_create_slices", "can_delete_slices",
    "can_add_aggregates", "can_remove_aggregates",
    "can_create_roles", "can_edit_roles",
]

DEFAULT_RESEARCHER_PERMISSIONS = [
    "can_view_project", "can_create_slices",
]

def list(request):
    '''Show list of projects'''

    qs = Project.objects.get_for_user(request.user)

    return list_detail.object_list(
        request,
        queryset=qs,
        template_name=TEMPLATE_PATH+"/list.html",
        template_object_name="project",
    )

@require_objs_permissions_for_view(
    perm_names=["can_delete_slices"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
    methods=["GET", "POST"],
)
@require_objs_permissions_for_view(
    perm_names=["can_delete_project"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
    methods=["GET", "POST"],
)
def delete(request, proj_id):
    '''Delete the project'''
    project = get_object_or_404(Project, id=proj_id)
    if request.method == "POST":
        for s in project.slice_set.all():
            s.stop(request.user)
    req = create_update.delete_object(
        request,
        model=Project,
        post_delete_redirect=reverse('home'),
        object_id=proj_id,
        template_name=TEMPLATE_PATH+"/confirm_delete.html",
    )
    if req.status_code == HttpResponseRedirect.status_code:
        DatedMessage.objects.post_message_to_user(
            "Successfully deleted project %s" % project.name,
            request.user, msg_type=DatedMessage.TYPE_SUCCESS)
    return req

@require_objs_permissions_for_view(
    perm_names=["can_view_project"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def detail(request, proj_id):
    '''Show information about the project'''
    project = get_object_or_404(Project, id=proj_id)
    role_reqs = ProjectRoleRequest.objects.filter(
        giver=request.user, requested_role__project=project)
    return list_detail.object_detail(
        request,
        Project.objects.all(),
        object_id=proj_id,
        template_name=TEMPLATE_PATH+"/detail.html",
        template_object_name="project",
        extra_context={
            "role_requests": role_reqs,
            "breadcrumbs": (
                ("Home", reverse("home")),
                ("Project %s" % project.name, reverse("project_detail", args=[project.id])),
            )
        }
    )

@require_objs_permissions_for_view(
    perm_names=["can_create_project"],
    permittee_func=get_user_from_req,
    target_func=get_queryset_from_class(Project),
)
def create(request):
    '''Create a new project'''

    def post_save(instance, created):
        # Create default roles in the project
        owner_role = ProjectRole.objects.create(
            name="owner",
            description=\
                "The 'owner' role is a special role that has permission to "
                "do everything "
                "in the project and can give the permissions to everyone. "
                "In addition every time a slice is created, users with "
                "the 'owner' role get full permissions over those.",
            project=instance,
        )
        for permission in DEFAULT_OWNER_PERMISSIONS:
            obj_perm = ObjectPermission.objects.\
                get_or_create_for_object_or_class(
                    permission, instance)[0]
            owner_role.obj_permissions.add(obj_perm)

        researcher_role = ProjectRole.objects.create(
            name="researcher",
            description=\
                "By default users with the 'researcher' role can only "
                "create slices and "
                "delete slices they created. They have full permissions over "
                "their created slices.",
                project=instance,
        )
        for permission in DEFAULT_RESEARCHER_PERMISSIONS:
            obj_perm = ObjectPermission.objects.\
                get_or_create_for_object_or_class(
                    permission, instance)[0]
            researcher_role.obj_permissions.add(obj_perm)

        # give the creator of the project an owner role
        owner_role.give_to_permittee(
            request.user,
            can_delegate=True,
        )

    def redirect(instance):
        return reverse("project_detail", args=[instance.id])

    return generic_crud(
        request, None,
        model=Project,
        form_class=ProjectCreateForm,
        template=TEMPLATE_PATH+"/create_update.html",
        post_save=post_save,
        redirect=redirect,
        template_object_name="project",
        extra_context={
            "breadcrumbs": (
                ("Home", reverse("home")),
                ("Create Project", request.path),
            ),
        },
        success_msg = lambda instance: "Successfully created project %s." % instance.name,
    )

@require_objs_permissions_for_view(
    perm_names=["can_view_project"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
@require_objs_permissions_for_view(
    perm_names=["can_edit_project"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
    methods=["POST"],
)
def update(request, proj_id, iframe=False):
    '''Update information about a project'''

    project = get_object_or_404(Project, id=proj_id)

    def redirect(instance):
        if iframe:
            return reverse("project_list")
        else:
            return reverse("project_detail", args=[instance.id])

    return generic_crud(
        request, proj_id,
        model=Project,
        form_class=ProjectCreateForm,
        template=TEMPLATE_PATH+"/create_update.html",
        redirect=redirect,
        template_object_name="project",
        extra_context={
            "breadcrumbs": (
                ("Home", reverse("home")),
                ("Project %s" % project.name, reverse("project_detail", args=[project.id])),
                ("Update Info", request.path),
            ),
        },
        success_msg = lambda instance: "Successfully updated project %s." % instance.name,
    )

@require_objs_permissions_for_view(
    perm_names=["can_add_aggregates"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def add_aggregate(request, proj_id):
    '''Add/remove aggregates to/from a project'''

    project = get_object_or_404(Project, id=proj_id)
    aggregate_list = Aggregate.objects.exclude(
        id__in=project.aggregates.all().values_list("id", flat=True))

    if request.method == "GET":
        return simple.direct_to_template(
            request, template=TEMPLATE_PATH+"/add_aggregates.html",
            extra_context={
                "aggregate_list": aggregate_list,
                "project": project,
                "breadcrumbs": (
                    ("Home", reverse("home")),
                    ("Project %s" % project.name, reverse("project_detail", args=[project.id])),
                    ("Add Project Aggregates", request.path),
                ),
            }
        )

    elif request.method == "POST":
        # check which submit button was pressed
        items = request.POST.items()
        agg_id = -1
        for i in items:
            if i[1] == "Select":
                try:
                    agg_id = int(i[0]) # there should be only one item
                except:
                    raise Http404
        if agg_id not in aggregate_list.values_list("id", flat=True):
            raise Http404
        aggregate = get_object_or_404(Aggregate, id=agg_id).as_leaf_class()
        return HttpResponseRedirect(aggregate.add_to_project(
            project, reverse("project_add_agg", args=[proj_id])))
    else:
        return HttpResponseNotAllowed("GET", "POST")

@require_objs_permissions_for_view(
    perm_names=["can_add_aggregates"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def update_aggregate(request, proj_id, agg_id):
    '''Update any info stored at the aggregate'''
    # TODO: This function might actually change the DB. So change to post
    project = get_object_or_404(Project, id=proj_id)
    aggregate = get_object_or_404(
        Aggregate, id=agg_id, id__in=project.aggregates.values_list(
            "id", flat=True)).as_leaf_class()
    return HttpResponseRedirect(aggregate.add_to_project(
        project, reverse("project_detail", args=[proj_id])))

@require_objs_permissions_for_view(
    perm_names=["can_remove_aggregates"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def remove_aggregate(request, proj_id, agg_id):
    """Remove the aggregate from the project"""
    # TODO: This function might actually change the DB. So change to post
    project = get_object_or_404(Project, id=proj_id)
    aggregate = get_object_or_404(
        Aggregate, id=agg_id, id__in=project.aggregates.values_list(
            "id", flat=True)).as_leaf_class()
    return HttpResponseRedirect(aggregate.remove_from_project(
        project, reverse("project_detail", args=[proj_id])))

@require_objs_permissions_for_view(
    perm_names=["can_add_members"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def add_member(request, proj_id):
    """Add a member to the project"""

    project = get_object_or_404(Project, id=proj_id)

    if request.method == "POST":
        form = AddMemberForm(project=project, giver=request.user, data=request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse("project_detail", args=[proj_id]))

    else:
        form = AddMemberForm(project=project, giver=request.user)

    return simple.direct_to_template(
        request,
        template=TEMPLATE_PATH+"/add_member.html",
        extra_context={
            "form": form,
            "project": project,
            "breadcrumbs": (
                ("Home", reverse("home")),
                ("Project %s" % project.name, reverse("project_detail", args=[project.id])),
                ("Add Member", request.path),
            ),
        },
    )

@require_objs_permissions_for_view(
    perm_names=["can_add_members"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def update_member(request, proj_id, user_id):
    """Update a member's roles"""

    project = get_object_or_404(Project, id=proj_id)
    member = get_object_or_404(User, id=user_id)

    if request.method == "POST":
        form = MemberForm(
            project=project, user=member,
            giver=request.user, data=request.POST)

        if form.is_valid():
            form.save()
            return HttpResponseRedirect(
                reverse("project_detail", args=[proj_id]))

    else:
        form = MemberForm(
            project=project, user=member,
            giver=request.user)

    return simple.direct_to_template(
        request,
        template=TEMPLATE_PATH+"/update_member.html",
        extra_context={
            "form": form,
            "project": project,
            "member": member,
            "breadcrumbs": (
                ("Home", reverse("home")),
                ("Project %s" % project.name, reverse("project_detail", args=[project.id])),
                ("Update Member %s" % member.username, request.path),
            ),
        },
    )

@require_objs_permissions_for_view(
    perm_names=["can_remove_members"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def remove_member(request, proj_id, user_id):
    """Kick a member out by stripping his roles"""

    project = get_object_or_404(Project, id=proj_id)
    member = get_object_or_404(User, id=user_id)

    if request.method == "POST":
        member = Permittee.objects.get_as_permittee(member)
        # Remove the roles
        for role in ProjectRole.objects.filter(project=project):
            role.remove_from_permittee(member)
        # Remove other permissions
        PermissionOwnership.objects.delete_all_for_target(project, member)
        return HttpResponseRedirect(
            reverse("project_detail", args=[proj_id]))

    return simple.direct_to_template(
        request,
        template=TEMPLATE_PATH+"/remove_member.html",
        extra_context={
            "project": project,
            "member": member,
            "breadcrumbs": (
                ("Home", reverse("home")),
                ("Project %s" % project.name, reverse("project_detail", args=[project.id])),
                ("Remove Member %s" % member.username, request.path),
            ),
        },
    )
<commit_msg>Add can_edit/start/stop_slices permissions in default roles<commit_after>'''
@author: jnaous
'''

from django.views.generic import list_detail, create_update, simple
from models import Project
from forms import ProjectCreateForm
from django.http import HttpResponseRedirect, HttpResponseNotAllowed, Http404
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404
from expedient.clearinghouse.aggregate.models import Aggregate
import logging
from expedient.common.utils.views import generic_crud
from expedient.common.messaging.models import DatedMessage
from django.db.models import Q
from expedient.common.permissions.decorators import require_objs_permissions_for_view
from expedient.common.permissions.utils import get_queryset, get_user_from_req,\
    get_queryset_from_class
from expedient.clearinghouse.roles.models import ProjectRole,\
    ProjectRoleRequest
from expedient.common.permissions.models import ObjectPermission,\
    PermissionOwnership, Permittee
from expedient.clearinghouse.project.forms import AddMemberForm, MemberForm
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType

logger = logging.getLogger("project.views")

TEMPLATE_PATH = "project"

DEFAULT_OWNER_PERMISSIONS = [
    "can_edit_project", "can_delete_project", "can_view_project",
    "can_add_members", "can_remove_members",
    "can_create_slices", "can_edit_slices", "can_delete_slices",
    "can_start_slices", "can_stop_slices",
    "can_add_aggregates", "can_remove_aggregates",
    "can_create_roles", "can_edit_roles",
]

DEFAULT_RESEARCHER_PERMISSIONS = [
    "can_view_project",
    "can_create_slices", "can_edit_slices", "can_delete_slices",
    "can_start_slices", "can_stop_slices",
]

def list(request):
    '''Show list of projects'''

    qs = Project.objects.get_for_user(request.user)

    return list_detail.object_list(
        request,
        queryset=qs,
        template_name=TEMPLATE_PATH+"/list.html",
        template_object_name="project",
    )

@require_objs_permissions_for_view(
    perm_names=["can_delete_slices"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
    methods=["GET", "POST"],
)
@require_objs_permissions_for_view(
    perm_names=["can_delete_project"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
    methods=["GET", "POST"],
)
def delete(request, proj_id):
    '''Delete the project'''
    project = get_object_or_404(Project, id=proj_id)
    if request.method == "POST":
        for s in project.slice_set.all():
            s.stop(request.user)
    req = create_update.delete_object(
        request,
        model=Project,
        post_delete_redirect=reverse('home'),
        object_id=proj_id,
        template_name=TEMPLATE_PATH+"/confirm_delete.html",
    )
    if req.status_code == HttpResponseRedirect.status_code:
        DatedMessage.objects.post_message_to_user(
            "Successfully deleted project %s" % project.name,
            request.user, msg_type=DatedMessage.TYPE_SUCCESS)
    return req

@require_objs_permissions_for_view(
    perm_names=["can_view_project"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def detail(request, proj_id):
    '''Show information about the project'''
    project = get_object_or_404(Project, id=proj_id)
    role_reqs = ProjectRoleRequest.objects.filter(
        giver=request.user, requested_role__project=project)
    return list_detail.object_detail(
        request,
        Project.objects.all(),
        object_id=proj_id,
        template_name=TEMPLATE_PATH+"/detail.html",
        template_object_name="project",
        extra_context={
            "role_requests": role_reqs,
            "breadcrumbs": (
                ("Home", reverse("home")),
                ("Project %s" % project.name, reverse("project_detail", args=[project.id])),
            )
        }
    )

@require_objs_permissions_for_view(
    perm_names=["can_create_project"],
    permittee_func=get_user_from_req,
    target_func=get_queryset_from_class(Project),
)
def create(request):
    '''Create a new project'''

    def post_save(instance, created):
        # Create default roles in the project
        owner_role = ProjectRole.objects.create(
            name="owner",
            description=\
                "The 'owner' role is a special role that has permission to "
                "do everything "
                "in the project and can give the permissions to everyone. "
                "In addition every time a slice is created, users with "
                "the 'owner' role get full permissions over those.",
            project=instance,
        )
        for permission in DEFAULT_OWNER_PERMISSIONS:
            obj_perm = ObjectPermission.objects.\
                get_or_create_for_object_or_class(
                    permission, instance)[0]
            owner_role.obj_permissions.add(obj_perm)

        researcher_role = ProjectRole.objects.create(
            name="researcher",
            description=\
                "By default users with the 'researcher' role can only "
                "create slices and "
                "delete slices they created. They have full permissions over "
                "their created slices.",
                project=instance,
        )
        for permission in DEFAULT_RESEARCHER_PERMISSIONS:
            obj_perm = ObjectPermission.objects.\
                get_or_create_for_object_or_class(
                    permission, instance)[0]
            researcher_role.obj_permissions.add(obj_perm)

        # give the creator of the project an owner role
        owner_role.give_to_permittee(
            request.user,
            can_delegate=True,
        )

    def redirect(instance):
        return reverse("project_detail", args=[instance.id])

    return generic_crud(
        request, None,
        model=Project,
        form_class=ProjectCreateForm,
        template=TEMPLATE_PATH+"/create_update.html",
        post_save=post_save,
        redirect=redirect,
        template_object_name="project",
        extra_context={
            "breadcrumbs": (
                ("Home", reverse("home")),
                ("Create Project", request.path),
            ),
        },
        success_msg = lambda instance: "Successfully created project %s." % instance.name,
    )

@require_objs_permissions_for_view(
    perm_names=["can_view_project"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
@require_objs_permissions_for_view(
    perm_names=["can_edit_project"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
    methods=["POST"],
)
def update(request, proj_id, iframe=False):
    '''Update information about a project'''

    project = get_object_or_404(Project, id=proj_id)

    def redirect(instance):
        if iframe:
            return reverse("project_list")
        else:
            return reverse("project_detail", args=[instance.id])

    return generic_crud(
        request, proj_id,
        model=Project,
        form_class=ProjectCreateForm,
        template=TEMPLATE_PATH+"/create_update.html",
        redirect=redirect,
        template_object_name="project",
        extra_context={
            "breadcrumbs": (
                ("Home", reverse("home")),
                ("Project %s" % project.name, reverse("project_detail", args=[project.id])),
                ("Update Info", request.path),
            ),
        },
        success_msg = lambda instance: "Successfully updated project %s." % instance.name,
    )

@require_objs_permissions_for_view(
    perm_names=["can_add_aggregates"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def add_aggregate(request, proj_id):
    '''Add/remove aggregates to/from a project'''

    project = get_object_or_404(Project, id=proj_id)
    aggregate_list = Aggregate.objects.exclude(
        id__in=project.aggregates.all().values_list("id", flat=True))

    if request.method == "GET":
        return simple.direct_to_template(
            request, template=TEMPLATE_PATH+"/add_aggregates.html",
            extra_context={
                "aggregate_list": aggregate_list,
                "project": project,
                "breadcrumbs": (
                    ("Home", reverse("home")),
                    ("Project %s" % project.name, reverse("project_detail", args=[project.id])),
                    ("Add Project Aggregates", request.path),
                ),
            }
        )

    elif request.method == "POST":
        # check which submit button was pressed
        items = request.POST.items()
        agg_id = -1
        for i in items:
            if i[1] == "Select":
                try:
                    agg_id = int(i[0]) # there should be only one item
                except:
                    raise Http404
        if agg_id not in aggregate_list.values_list("id", flat=True):
            raise Http404
        aggregate = get_object_or_404(Aggregate, id=agg_id).as_leaf_class()
        return HttpResponseRedirect(aggregate.add_to_project(
            project, reverse("project_add_agg", args=[proj_id])))
    else:
        return HttpResponseNotAllowed("GET", "POST")

@require_objs_permissions_for_view(
    perm_names=["can_add_aggregates"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def update_aggregate(request, proj_id, agg_id):
    '''Update any info stored at the aggregate'''
    # TODO: This function might actually change the DB. So change to post
    project = get_object_or_404(Project, id=proj_id)
    aggregate = get_object_or_404(
        Aggregate, id=agg_id, id__in=project.aggregates.values_list(
            "id", flat=True)).as_leaf_class()
    return HttpResponseRedirect(aggregate.add_to_project(
        project, reverse("project_detail", args=[proj_id])))

@require_objs_permissions_for_view(
    perm_names=["can_remove_aggregates"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def remove_aggregate(request, proj_id, agg_id):
    """Remove the aggregate from the project"""
    # TODO: This function might actually change the DB. So change to post
    project = get_object_or_404(Project, id=proj_id)
    aggregate = get_object_or_404(
        Aggregate, id=agg_id, id__in=project.aggregates.values_list(
            "id", flat=True)).as_leaf_class()
    return HttpResponseRedirect(aggregate.remove_from_project(
        project, reverse("project_detail", args=[proj_id])))

@require_objs_permissions_for_view(
    perm_names=["can_add_members"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def add_member(request, proj_id):
    """Add a member to the project"""

    project = get_object_or_404(Project, id=proj_id)

    if request.method == "POST":
        form = AddMemberForm(project=project, giver=request.user, data=request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse("project_detail", args=[proj_id]))

    else:
        form = AddMemberForm(project=project, giver=request.user)

    return simple.direct_to_template(
        request,
        template=TEMPLATE_PATH+"/add_member.html",
        extra_context={
            "form": form,
            "project": project,
            "breadcrumbs": (
                ("Home", reverse("home")),
                ("Project %s" % project.name, reverse("project_detail", args=[project.id])),
                ("Add Member", request.path),
            ),
        },
    )

@require_objs_permissions_for_view(
    perm_names=["can_add_members"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def update_member(request, proj_id, user_id):
    """Update a member's roles"""

    project = get_object_or_404(Project, id=proj_id)
    member = get_object_or_404(User, id=user_id)

    if request.method == "POST":
        form = MemberForm(
            project=project, user=member,
            giver=request.user, data=request.POST)

        if form.is_valid():
            form.save()
            return HttpResponseRedirect(
                reverse("project_detail", args=[proj_id]))

    else:
        form = MemberForm(
            project=project, user=member,
            giver=request.user)

    return simple.direct_to_template(
        request,
        template=TEMPLATE_PATH+"/update_member.html",
        extra_context={
            "form": form,
            "project": project,
            "member": member,
            "breadcrumbs": (
                ("Home", reverse("home")),
                ("Project %s" % project.name, reverse("project_detail", args=[project.id])),
                ("Update Member %s" % member.username, request.path),
            ),
        },
    )

@require_objs_permissions_for_view(
    perm_names=["can_remove_members"],
    permittee_func=get_user_from_req,
    target_func=get_queryset(Project, "proj_id"),
)
def remove_member(request, proj_id, user_id):
    """Kick a member out by stripping his roles"""

    project = get_object_or_404(Project, id=proj_id)
    member = get_object_or_404(User, id=user_id)

    if request.method == "POST":
        member = Permittee.objects.get_as_permittee(member)
        # Remove the roles
        for role in ProjectRole.objects.filter(project=project):
            role.remove_from_permittee(member)
        # Remove other permissions
        PermissionOwnership.objects.delete_all_for_target(project, member)
        return HttpResponseRedirect(
            reverse("project_detail", args=[proj_id]))

    return simple.direct_to_template(
        request,
        template=TEMPLATE_PATH+"/remove_member.html",
        extra_context={
            "project": project,
            "member": member,
            "breadcrumbs": (
                ("Home", reverse("home")),
                ("Project %s" % project.name, reverse("project_detail", args=[project.id])),
                ("Remove Member %s" % member.username, request.path),
            ),
        },
    )
<|endoftext|>


<commit_before># Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import annotations

import os.path
from dataclasses import dataclass

from pants.backend.go.util_rules.assembly import (
    AssemblyPostCompilation,
    AssemblyPostCompilationRequest,
    AssemblyPreCompilationRequest,
    FallibleAssemblyPreCompilation,
)
from pants.backend.go.util_rules.import_analysis import ImportConfig, ImportConfigRequest
from pants.backend.go.util_rules.sdk import GoSdkProcess
from pants.engine.engine_aware import EngineAwareParameter, EngineAwareReturnType
from pants.engine.fs import AddPrefix, Digest, MergeDigests
from pants.engine.process import FallibleProcessResult
from pants.engine.rules import Get, MultiGet, collect_rules, rule
from pants.util.frozendict import FrozenDict
from pants.util.logging import LogLevel
from pants.util.strutil import path_safe

class BuildGoPackageRequest(EngineAwareParameter):
    def __init__(
        self,
        *,
        import_path: str,
        digest: Digest,
        subpath: str,
        go_file_names: tuple[str, ...],
        s_file_names: tuple[str, ...],
        direct_dependencies: tuple[BuildGoPackageRequest, ...],
        minimum_go_version: str | None,
        for_tests: bool = False,
    ) -> None:
        """Build a package and its dependencies as `__pkg__.a` files.

        Instances of this class form a structure-shared DAG, and so a hashcode is pre-computed for
        the recursive portion.
        """

        self.import_path = import_path
        self.digest = digest
        self.subpath = subpath
        self.go_file_names = go_file_names
        self.s_file_names = s_file_names
        self.direct_dependencies = direct_dependencies
        self.minimum_go_version = minimum_go_version
        self.for_tests = for_tests
        self._hashcode = hash(
            (
                self.import_path,
                self.digest,
                self.subpath,
                self.go_file_names,
                self.s_file_names,
                self.direct_dependencies,
                self.minimum_go_version,
                self.for_tests,
            )
        )

    def __repr__(self) -> str:
        # NB: We must override the default `__repr__` so that `direct_dependencies` does not
        # traverse into transitive dependencies, which was pathologically slow.
        return (
            f"{self.__class__}("
            f"import_path={repr(self.import_path)}, "
            f"digest={self.digest}, "
            f"subpath={self.subpath}, "
            f"go_file_names={self.go_file_names}, "
            f"go_file_names={self.s_file_names}, "
            f"direct_dependencies={[dep.import_path for dep in self.direct_dependencies]}, "
            f"minimum_go_version={self.minimum_go_version}, "
            f"for_tests={self.for_tests}"
            ")"
        )

    def __hash__(self) -> int:
        return self._hashcode

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        return (
            self._hashcode == other._hashcode
            and self.import_path == other.import_path
            and self.digest == other.digest
            and self.subpath == other.subpath
            and self.go_file_names == other.go_file_names
            and self.s_file_names == other.s_file_names
            and self.minimum_go_version == other.minimum_go_version
            and self.for_tests == other.for_tests
            # TODO: Use a recursive memoized __eq__ if this ever shows up in profiles.
            and self.direct_dependencies == other.direct_dependencies
        )

    def debug_hint(self) -> str | None:
        return self.import_path

@dataclass(frozen=True)
class FallibleBuildGoPackageRequest(EngineAwareParameter, EngineAwareReturnType):
    """Request to build a package, but fallible if determining the request metadata failed.

    When creating "synthetic" packages, use `GoPackageRequest` directly. This type is only intended
    for determining the package metadata of user code, which may fail to be analyzed.
    """

    request: BuildGoPackageRequest | None
    import_path: str
    exit_code: int = 0
    stderr: str | None = None

    def level(self) -> LogLevel:
        return LogLevel.ERROR if self.exit_code != 0 else LogLevel.DEBUG

    def message(self) -> str:
        message = self.import_path
        message += (
            " succeeded." if self.exit_code == 0 else f" failed (exit code {self.exit_code})."
        )
        if self.stderr:
            message += f"\n{self.stderr}"
        return message

    def cacheable(self) -> bool:
        # Failed compile outputs should be re-rendered in every run.
        return self.exit_code == 0

@dataclass(frozen=True)
class FallibleBuiltGoPackage(EngineAwareReturnType):
    """Fallible version of `BuiltGoPackage` with error details."""

    output: BuiltGoPackage | None
    import_path: str
    exit_code: int = 0
    stdout: str | None = None
    stderr: str | None = None

    def level(self) -> LogLevel:
        return LogLevel.ERROR if self.exit_code != 0 else LogLevel.DEBUG

    def message(self) -> str:
        message = self.import_path
        message += (
            " succeeded." if self.exit_code == 0 else f" failed (exit code {self.exit_code})."
        )
        if self.stdout:
            message += f"\n{self.stdout}"
        if self.stderr:
            message += f"\n{self.stderr}"
        return message

    def cacheable(self) -> bool:
        # Failed compile outputs should be re-rendered in every run.
        return self.exit_code == 0

@dataclass(frozen=True)
class BuiltGoPackage:
    """A package and its dependencies compiled as `__pkg__.a` files.

    The packages are arranged into `__pkgs__/{path_safe(import_path)}/__pkg__.a`.
    """

    digest: Digest
    import_paths_to_pkg_a_files: FrozenDict[str, str]

# NB: We must have a description for the streaming of this rule to work properly
# (triggered by `FallibleBuiltGoPackage` subclassing `EngineAwareReturnType`).
@rule(desc="Compile with Go", level=LogLevel.DEBUG)
async def build_go_package(request: BuildGoPackageRequest) -> FallibleBuiltGoPackage:
    maybe_built_deps = await MultiGet(
        Get(FallibleBuiltGoPackage, BuildGoPackageRequest, build_request)
        for build_request in request.direct_dependencies
    )

    import_paths_to_pkg_a_files: dict[str, str] = {}
    dep_digests = []
    for maybe_dep in maybe_built_deps:
        if maybe_dep.output is None:
            return maybe_dep
        dep = maybe_dep.output
        import_paths_to_pkg_a_files.update(dep.import_paths_to_pkg_a_files)
        dep_digests.append(dep.digest)

    merged_deps_digest, import_config = await MultiGet(
        Get(Digest, MergeDigests(dep_digests)),
        Get(ImportConfig, ImportConfigRequest(FrozenDict(import_paths_to_pkg_a_files))),
    )

    input_digest = await Get(
        Digest, MergeDigests([merged_deps_digest, import_config.digest, request.digest])
    )

    assembly_digests = None
    symabis_path = None
    if request.s_file_names:
        assembly_setup = await Get(
            FallibleAssemblyPreCompilation,
            AssemblyPreCompilationRequest(input_digest, request.s_file_names, request.subpath),
        )
        if assembly_setup.result is None:
            return FallibleBuiltGoPackage(
                None,
                request.import_path,
                assembly_setup.exit_code,
                stdout=assembly_setup.stdout,
                stderr=assembly_setup.stderr,
            )
        input_digest = assembly_setup.result.merged_compilation_input_digest
        assembly_digests = assembly_setup.result.assembly_digests
        symabis_path = "./symabis"

    compile_args = [
        "tool",
        "compile",
        "-o",
        "__pkg__.a",
        "-pack",
        "-p",
        request.import_path,
        "-importcfg",
        import_config.CONFIG_PATH,
        # See https://github.com/golang/go/blob/f229e7031a6efb2f23241b5da000c3b3203081d6/src/cmd/go/internal/work/gc.go#L79-L100
        # for why Go sets the default to 1.16.
        f"-lang=go{request.minimum_go_version or '1.16'}",
    ]

    if symabis_path:
        compile_args.extend(["-symabis", symabis_path])
    relativized_sources = (
        f"./{request.subpath}/{name}" if request.subpath else f"./{name}"
        for name in request.go_file_names
    )
    compile_args.extend(["--", *relativized_sources])
    compile_result = await Get(
        FallibleProcessResult,
        GoSdkProcess(
            input_digest=input_digest,
            command=tuple(compile_args),
            description=f"Compile Go package: {request.import_path}",
            output_files=("__pkg__.a",),
        ),
    )
    if compile_result.exit_code != 0:
        return FallibleBuiltGoPackage(
            None,
            request.import_path,
            compile_result.exit_code,
            stdout=compile_result.stdout.decode("utf-8"),
            stderr=compile_result.stderr.decode("utf-8"),
        )

    compilation_digest = compile_result.output_digest
    if assembly_digests:
        assembly_result = await Get(
            AssemblyPostCompilation,
            AssemblyPostCompilationRequest(
                compilation_digest,
                assembly_digests,
                request.s_file_names,
                request.subpath,
            ),
        )
        if assembly_result.result.exit_code != 0:
            return FallibleBuiltGoPackage(
                None,
                request.import_path,
                assembly_result.result.exit_code,
                stdout=assembly_result.result.stdout.decode("utf-8"),
                stderr=assembly_result.result.stderr.decode("utf-8"),
            )
        assert assembly_result.merged_output_digest
        compilation_digest = assembly_result.merged_output_digest

    path_prefix = os.path.join("__pkgs__", path_safe(request.import_path))
    import_paths_to_pkg_a_files[request.import_path] = os.path.join(path_prefix, "__pkg__.a")
    output_digest = await Get(Digest, AddPrefix(compilation_digest, path_prefix))
    merged_result_digest = await Get(Digest, MergeDigests([*dep_digests, output_digest]))

    output = BuiltGoPackage(merged_result_digest, FrozenDict(import_paths_to_pkg_a_files))
    return FallibleBuiltGoPackage(output, request.import_path)

@rule
def required_built_go_package(fallible_result: FallibleBuiltGoPackage) -> BuiltGoPackage:
    if fallible_result.output is not None:
        return fallible_result.output
    raise Exception(
        f"Failed to compile {fallible_result.import_path}:\n"
        f"{fallible_result.stdout}\n{fallible_result.stderr}"
    )

def rules():
    return collect_rules()
<commit_msg>[internal] go: pass `-complete` flag to compiler(#13507)<commit_after># Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import annotations

import os.path
from dataclasses import dataclass

from pants.backend.go.util_rules.assembly import (
    AssemblyPostCompilation,
    AssemblyPostCompilationRequest,
    AssemblyPreCompilationRequest,
    FallibleAssemblyPreCompilation,
)
from pants.backend.go.util_rules.import_analysis import ImportConfig, ImportConfigRequest
from pants.backend.go.util_rules.sdk import GoSdkProcess
from pants.engine.engine_aware import EngineAwareParameter, EngineAwareReturnType
from pants.engine.fs import AddPrefix, Digest, MergeDigests
from pants.engine.process import FallibleProcessResult
from pants.engine.rules import Get, MultiGet, collect_rules, rule
from pants.util.frozendict import FrozenDict
from pants.util.logging import LogLevel
from pants.util.strutil import path_safe

class BuildGoPackageRequest(EngineAwareParameter):
    def __init__(
        self,
        *,
        import_path: str,
        digest: Digest,
        subpath: str,
        go_file_names: tuple[str, ...],
        s_file_names: tuple[str, ...],
        direct_dependencies: tuple[BuildGoPackageRequest, ...],
        minimum_go_version: str | None,
        for_tests: bool = False,
    ) -> None:
        """Build a package and its dependencies as `__pkg__.a` files.

        Instances of this class form a structure-shared DAG, and so a hashcode is pre-computed for
        the recursive portion.
        """

        self.import_path = import_path
        self.digest = digest
        self.subpath = subpath
        self.go_file_names = go_file_names
        self.s_file_names = s_file_names
        self.direct_dependencies = direct_dependencies
        self.minimum_go_version = minimum_go_version
        self.for_tests = for_tests
        self._hashcode = hash(
            (
                self.import_path,
                self.digest,
                self.subpath,
                self.go_file_names,
                self.s_file_names,
                self.direct_dependencies,
                self.minimum_go_version,
                self.for_tests,
            )
        )

    def __repr__(self) -> str:
        # NB: We must override the default `__repr__` so that `direct_dependencies` does not
        # traverse into transitive dependencies, which was pathologically slow.
        return (
            f"{self.__class__}("
            f"import_path={repr(self.import_path)}, "
            f"digest={self.digest}, "
            f"subpath={self.subpath}, "
            f"go_file_names={self.go_file_names}, "
            f"go_file_names={self.s_file_names}, "
            f"direct_dependencies={[dep.import_path for dep in self.direct_dependencies]}, "
            f"minimum_go_version={self.minimum_go_version}, "
            f"for_tests={self.for_tests}"
            ")"
        )

    def __hash__(self) -> int:
        return self._hashcode

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        return (
            self._hashcode == other._hashcode
            and self.import_path == other.import_path
            and self.digest == other.digest
            and self.subpath == other.subpath
            and self.go_file_names == other.go_file_names
            and self.s_file_names == other.s_file_names
            and self.minimum_go_version == other.minimum_go_version
            and self.for_tests == other.for_tests
            # TODO: Use a recursive memoized __eq__ if this ever shows up in profiles.
            and self.direct_dependencies == other.direct_dependencies
        )

    def debug_hint(self) -> str | None:
        return self.import_path

@dataclass(frozen=True)
class FallibleBuildGoPackageRequest(EngineAwareParameter, EngineAwareReturnType):
    """Request to build a package, but fallible if determining the request metadata failed.

    When creating "synthetic" packages, use `GoPackageRequest` directly. This type is only intended
    for determining the package metadata of user code, which may fail to be analyzed.
    """

    request: BuildGoPackageRequest | None
    import_path: str
    exit_code: int = 0
    stderr: str | None = None

    def level(self) -> LogLevel:
        return LogLevel.ERROR if self.exit_code != 0 else LogLevel.DEBUG

    def message(self) -> str:
        message = self.import_path
        message += (
            " succeeded." if self.exit_code == 0 else f" failed (exit code {self.exit_code})."
        )
        if self.stderr:
            message += f"\n{self.stderr}"
        return message

    def cacheable(self) -> bool:
        # Failed compile outputs should be re-rendered in every run.
        return self.exit_code == 0

@dataclass(frozen=True)
class FallibleBuiltGoPackage(EngineAwareReturnType):
    """Fallible version of `BuiltGoPackage` with error details."""

    output: BuiltGoPackage | None
    import_path: str
    exit_code: int = 0
    stdout: str | None = None
    stderr: str | None = None

    def level(self) -> LogLevel:
        return LogLevel.ERROR if self.exit_code != 0 else LogLevel.DEBUG

    def message(self) -> str:
        message = self.import_path
        message += (
            " succeeded." if self.exit_code == 0 else f" failed (exit code {self.exit_code})."
        )
        if self.stdout:
            message += f"\n{self.stdout}"
        if self.stderr:
            message += f"\n{self.stderr}"
        return message

    def cacheable(self) -> bool:
        # Failed compile outputs should be re-rendered in every run.
        return self.exit_code == 0

@dataclass(frozen=True)
class BuiltGoPackage:
    """A package and its dependencies compiled as `__pkg__.a` files.

    The packages are arranged into `__pkgs__/{path_safe(import_path)}/__pkg__.a`.
    """

    digest: Digest
    import_paths_to_pkg_a_files: FrozenDict[str, str]

# NB: We must have a description for the streaming of this rule to work properly
# (triggered by `FallibleBuiltGoPackage` subclassing `EngineAwareReturnType`).
@rule(desc="Compile with Go", level=LogLevel.DEBUG)
async def build_go_package(request: BuildGoPackageRequest) -> FallibleBuiltGoPackage:
    maybe_built_deps = await MultiGet(
        Get(FallibleBuiltGoPackage, BuildGoPackageRequest, build_request)
        for build_request in request.direct_dependencies
    )

    import_paths_to_pkg_a_files: dict[str, str] = {}
    dep_digests = []
    for maybe_dep in maybe_built_deps:
        if maybe_dep.output is None:
            return maybe_dep
        dep = maybe_dep.output
        import_paths_to_pkg_a_files.update(dep.import_paths_to_pkg_a_files)
        dep_digests.append(dep.digest)

    merged_deps_digest, import_config = await MultiGet(
        Get(Digest, MergeDigests(dep_digests)),
        Get(ImportConfig, ImportConfigRequest(FrozenDict(import_paths_to_pkg_a_files))),
    )

    input_digest = await Get(
        Digest, MergeDigests([merged_deps_digest, import_config.digest, request.digest])
    )

    assembly_digests = None
    symabis_path = None
    if request.s_file_names:
        assembly_setup = await Get(
            FallibleAssemblyPreCompilation,
            AssemblyPreCompilationRequest(input_digest, request.s_file_names, request.subpath),
        )
        if assembly_setup.result is None:
            return FallibleBuiltGoPackage(
                None,
                request.import_path,
                assembly_setup.exit_code,
                stdout=assembly_setup.stdout,
                stderr=assembly_setup.stderr,
            )
        input_digest = assembly_setup.result.merged_compilation_input_digest
        assembly_digests = assembly_setup.result.assembly_digests
        symabis_path = "./symabis"

    compile_args = [
        "tool",
        "compile",
        "-o",
        "__pkg__.a",
        "-pack",
        "-p",
        request.import_path,
        "-importcfg",
        import_config.CONFIG_PATH,
        # See https://github.com/golang/go/blob/f229e7031a6efb2f23241b5da000c3b3203081d6/src/cmd/go/internal/work/gc.go#L79-L100
        # for why Go sets the default to 1.16.
        f"-lang=go{request.minimum_go_version or '1.16'}",
    ]

    if symabis_path:
        compile_args.extend(["-symabis", symabis_path])
    if not request.s_file_names:
        # If there are no non-Go sources, then pass -complete flag which tells the compiler that the provided
        # Go files are the entire package.
        compile_args.append("-complete")
    relativized_sources = (
        f"./{request.subpath}/{name}" if request.subpath else f"./{name}"
        for name in request.go_file_names
    )
    compile_args.extend(["--", *relativized_sources])
    compile_result = await Get(
        FallibleProcessResult,
        GoSdkProcess(
            input_digest=input_digest,
            command=tuple(compile_args),
            description=f"Compile Go package: {request.import_path}",
            output_files=("__pkg__.a",),
        ),
    )
    if compile_result.exit_code != 0:
        return FallibleBuiltGoPackage(
            None,
            request.import_path,
            compile_result.exit_code,
            stdout=compile_result.stdout.decode("utf-8"),
            stderr=compile_result.stderr.decode("utf-8"),
        )

    compilation_digest = compile_result.output_digest
    if assembly_digests:
        assembly_result = await Get(
            AssemblyPostCompilation,
            AssemblyPostCompilationRequest(
                compilation_digest,
                assembly_digests,
                request.s_file_names,
                request.subpath,
            ),
        )
        if assembly_result.result.exit_code != 0:
            return FallibleBuiltGoPackage(
                None,
                request.import_path,
                assembly_result.result.exit_code,
                stdout=assembly_result.result.stdout.decode("utf-8"),
                stderr=assembly_result.result.stderr.decode("utf-8"),
            )
        assert assembly_result.merged_output_digest
        compilation_digest = assembly_result.merged_output_digest

    path_prefix = os.path.join("__pkgs__", path_safe(request.import_path))
    import_paths_to_pkg_a_files[request.import_path] = os.path.join(path_prefix, "__pkg__.a")
    output_digest = await Get(Digest, AddPrefix(compilation_digest, path_prefix))
    merged_result_digest = await Get(Digest, MergeDigests([*dep_digests, output_digest]))

    output = BuiltGoPackage(merged_result_digest, FrozenDict(import_paths_to_pkg_a_files))
    return FallibleBuiltGoPackage(output, request.import_path)

@rule
def required_built_go_package(fallible_result: FallibleBuiltGoPackage) -> BuiltGoPackage:
    if fallible_result.output is not None:
        return fallible_result.output
    raise Exception(
        f"Failed to compile {fallible_result.import_path}:\n"
        f"{fallible_result.stdout}\n{fallible_result.stderr}"
    )

def rules():
    return collect_rules()
<|endoftext|>