Ejemplo n.º 1
0
    def _split_plugin_spec(plugin_spec: str, plugins: dict) -> Tuple[str, str]:
        parts = plugin_spec.split(PLUGIN_SPEC_SEP)
        if len(parts) != 2:
            raise plug.PlugError(f"malformed plugin spec '{plugin_spec}'")

        name, version = parts

        if name not in plugins:
            raise plug.PlugError(f"no plugin with name '{name}'")
        elif version not in plugins[name]["versions"]:
            raise plug.PlugError(f"plugin '{name}' has no version '{version}'")

        return name, version
Ejemplo n.º 2
0
def _convert_task_exceptions(task):
    """Catch task exceptions and re-raise or convert into something more
    appropriate for the user. Only plug.PlugErrors will be let through without
    modification.
    """
    try:
        yield
    except plug.PlugError as exc:
        raise plug.PlugError("A task from the module '{}' crashed: {}".format(
            task.act.__module__, str(exc)))
    except Exception as exc:
        raise plug.PlugError(
            "A task from the module '{}' crashed unexpectedly. "
            "This is a bug, please report it to the plugin "
            "author.".format(task.act.__module__)) from exc
Ejemplo n.º 3
0
    def command(self) -> None:
        """Install a plugin."""
        plugins = disthelpers.get_plugins_json()
        installed_plugins = disthelpers.get_installed_plugins()
        active_plugins = disthelpers.get_active_plugins()

        if self.local:
            abspath = self.local.absolute()
            if not abspath.exists():
                raise plug.PlugError(f"no such file or directory: '{abspath}'")

            _install_local_plugin(abspath, installed_plugins)
        else:
            plug.echo("Available plugins:")
            _list_all_plugins(plugins, installed_plugins, active_plugins)
            name, version = _select_plugin(plugins)

            if name in installed_plugins:
                _uninstall_plugin(name, installed_plugins)

            plug.echo(f"Installing {name}@{version}")
            _install_plugin(name, version, plugins)

            plug.echo(f"Successfully installed {name}@{version}")

            installed_plugins[name] = dict(version=version)
            disthelpers.write_installed_plugins(installed_plugins)
Ejemplo n.º 4
0
def _parse_from_classpath(pattern: str, classpath: str) -> pathlib.Path:
    matches = re.search(pattern, classpath)
    if not matches:
        raise plug.PlugError(
            f"expected to find match for '{pattern}' on the CLASSPATH variable"
        )
    return matches.group(0) if matches else None
Ejemplo n.º 5
0
def _parse_yamliny(students_file: pathlib.Path) -> dict:
    try:
        return yamliny.loads(
            students_file.read_text(encoding=sys.getdefaultencoding())
        )
    except yamliny.YamlinyError as exc:
        raise plug.PlugError(f"Parse error '{students_file}': {exc}") from exc
Ejemplo n.º 6
0
    def _install_plugin(
        self, plugins: dict, installed_plugins: dict, active_plugins: List[str]
    ) -> None:
        if self.local:
            abspath = self.local.absolute()
            if not abspath.exists():
                raise plug.PlugError(f"no such file or directory: '{abspath}'")

            _install_local_plugin(abspath, installed_plugins)
        elif self.git_url:
            _install_plugin_from_git_repo(self.git_url, installed_plugins)
        else:
            plug.echo("Available plugins:")

            if self.plugin_spec:
                # non-interactive install
                name, version = self._split_plugin_spec(
                    self.plugin_spec, plugins
                )
            else:
                # interactive install
                _list_all_plugins(plugins, installed_plugins, active_plugins)
                name, version = _select_plugin(plugins)

            if name in installed_plugins:
                _uninstall_plugin(name, installed_plugins)

            plug.echo(f"Installing {name}{PLUGIN_SPEC_SEP}{version}")
            _install_plugin(name, version, plugins)

            plug.echo(f"Successfully installed {name}@{version}")

            installed_plugins[name] = dict(version=version)
            disthelpers.write_installed_plugins(installed_plugins)
Ejemplo n.º 7
0
    def __init__(self, base_url: str, token: str, org_name: str, user: str):
        """Set up the GitHub API object.

        Args:
            base_url: The base url to a GitHub REST api (e.g.
            https://api.github.com for GitHub or https://<HOST>/api/v3 for
            Enterprise).
            token: A GitHub access token.
            user: Name of the current user of the API.
            org_name: Name of the target organization.
        """

        # see https://docs.github.com/en/rest/guides/best-practices-for-integrators#dealing-with-secondary-rate-limits
        http.rate_limit_modify_requests(base_url, rate_limit_in_seconds=1)

        if not user:
            raise TypeError("argument 'user' must not be empty")
        if not (base_url == "https://api.github.com"
                or base_url.endswith("/api/v3")):
            raise plug.PlugError(
                "invalid base url, should either be https://api.github.com or "
                "end with '/api/v3'. See the docs: "
                "https://repobee.readthedocs.io/en/stable/"
                "getting_started.html#configure-repobee-for-the-target"
                "-organization-show-config-and-verify-settings")
        self._github = github.Github(login_or_token=token, base_url=base_url)
        self._org_name = org_name
        self._base_url = base_url
        self._token = token
        self._user = user
        with _try_api_request():
            self._org = self._github.get_organization(self._org_name)
Ejemplo n.º 8
0
    def command(self) -> None:
        """Uninstall a plugin."""
        installed_plugins = {
            name: attrs
            for name, attrs in disthelpers.get_installed_plugins().items()
            if not attrs.get("builtin")
        }

        if self.plugin_name:
            # non-interactive uninstall
            if self.plugin_name not in installed_plugins:
                raise plug.PlugError(
                    f"no plugin '{self.plugin_name}' installed"
                )
            selected_plugin_name = self.plugin_name
        else:
            # interactive uninstall
            if not installed_plugins:
                plug.echo("No plugins installed")
                return

            plug.echo("Installed plugins:")
            _list_installed_plugins(
                installed_plugins, disthelpers.get_active_plugins()
            )

            selected_plugin_name = bullet.Bullet(
                prompt="Select a plugin to uninstall:",
                choices=list(installed_plugins.keys()),
            ).launch()

        _uninstall_plugin(selected_plugin_name, installed_plugins)
Ejemplo n.º 9
0
    def command(self) -> None:
        """Activate a plugin."""
        installed_plugins = disthelpers.get_installed_plugins()
        active = disthelpers.get_active_plugins()

        names = list(installed_plugins.keys()) + list(
            disthelpers.get_builtin_plugins().keys()
        )

        if self.plugin_name:
            # non-interactive activate
            if self.plugin_name not in names:
                raise plug.PlugError(
                    f"no plugin named '{self.plugin_name}' installed"
                )
            selection = (
                active + [self.plugin_name]
                if self.plugin_name not in active
                else list(set(active) - {self.plugin_name})
            )
        else:
            # interactive activate
            default = [i for i, name in enumerate(names) if name in active]
            selection = bullet.Check(
                choices=names,
                prompt="Select plugins to activate (space to check/un-check, "
                "enter to confirm selection):",
            ).launch(default=default)

        disthelpers.write_active_plugins(selection)

        self._echo_state_change(active_before=active, active_after=selection)
Ejemplo n.º 10
0
def extract_list_issues_results(
        repo_name, hook_results: List[plug.HookResult]) -> plug.HookResult:
    for result in hook_results:
        if result.hook == "list-issues":
            return result
    raise plug.PlugError(
        "hook results for {} does not contain 'list-issues' result".format(
            repo_name))
Ejemplo n.º 11
0
def _pip_uninstall_plugin(plugin_name: str) -> None:
    uninstalled = (
        disthelpers.pip(
            "uninstall", "-y", f"{PLUGIN_PREFIX}{plugin_name}"
        ).returncode
        == 0
    )
    if not uninstalled:
        raise plug.PlugError(f"could not uninstall {plugin_name}")
Ejemplo n.º 12
0
def _install_plugin(name: str, version: str, plugins: dict) -> None:
    install_url = f"git+{plugins[name]['url']}@{version}"
    install_proc = disthelpers.pip(
        "install",
        install_url,
        f"repobee=={__version__}",  # force RepoBee to stay the same version
        upgrade=True,
    )
    if install_proc.returncode != 0:
        raise plug.PlugError(f"could not install {name} {version}")
Ejemplo n.º 13
0
def read_grades_file(grades_file: pathlib.Path):
    if not grades_file.is_file():
        raise plug.PlugError(f"no such file: {str(grades_file)}")
    with open(
        grades_file, encoding=sys.getdefaultencoding(), mode="r"
    ) as file:
        grades_file_contents = [
            [cell.strip() for cell in row]
            for row in csv.reader(file, delimiter=",")
        ]
        return grades_file_contents[0], grades_file_contents[1:]
Ejemplo n.º 14
0
 def _check_jars_exist(self):
     """Check that the specified jar files actually exist."""
     junit_path = self._junit_path or self._parse_from_classpath(
         _junit4_runner.JUNIT_JAR)
     hamcrest_path = self._hamcrest_path or self._parse_from_classpath(
         _junit4_runner.HAMCREST_JAR)
     for raw_path in (junit_path, hamcrest_path):
         if not pathlib.Path(raw_path).is_file():
             raise plug.PlugError(
                 "{} is not a file, please check the filepath you "
                 "specified".format(raw_path))
Ejemplo n.º 15
0
def _parse_multi_issues_file(
    issues_file: pathlib.Path, ) -> Iterable[Tuple[str, plug.Issue]]:
    with open(str(issues_file), mode="r",
              encoding=sys.getdefaultencoding()) as file:
        lines = list(file.readlines())

    if not lines or not re.match(BEGIN_ISSUE_PATTERN, lines[0], re.IGNORECASE):
        raise plug.PlugError(
            "first line of multi issues file not #ISSUE# line")

    issue_blocks = _extract_issue_blocks(lines)
    return list(_extract_issues(issue_blocks, lines))
Ejemplo n.º 16
0
    def command(self) -> None:
        hook_results_file = pathlib.Path(self.hook_results_file).resolve()
        if not hook_results_file.exists():
            raise plug.PlugError(f"no such file: {str(hook_results_file)}")

        contents = hook_results_file.read_text(
            encoding=sys.getdefaultencoding())
        hook_results_mapping = plug.json_to_result_mapping(contents)
        selected_hook_results = _filter_hook_results(hook_results_mapping,
                                                     self.args.students,
                                                     self.args.assignments)
        plug.echo(formatters.format_hook_results_output(selected_hook_results))
Ejemplo n.º 17
0
def get_plugins_json(url: str = "https://repobee.org/plugins.json") -> dict:
    """Fetch and parse the plugins.json file.

    Args:
        url: URL to the plugins.json file.
    Returns:
        A dictionary with the contents of the plugins.json file.
    """
    resp = requests.get(url)
    if resp.status_code != 200:
        plug.log.error(resp.content.decode("utf8"))
        raise plug.PlugError(f"could not fetch plugins.json from '{url}'")
    return resp.json()
Ejemplo n.º 18
0
 def _parse_from_classpath(self, filename: str) -> pathlib.Path:
     """Parse the full path to the given filename from the classpath, if
     it's on the classpath at all. If there are several hits, take the first
     one, and if there are none, raise a PlugError.
     """
     matches = [
         pathlib.Path(p) for p in self._classpath.split(os.pathsep)
         if p.endswith(filename)
     ]
     if not matches:
         raise plug.PlugError(
             "expected to find {} on the CLASSPATH variable".format(
                 filename))
     return matches[0] if matches else None
Ejemplo n.º 19
0
def _install_plugin_from_git_repo(
    repo_url: str, installed_plugins: dict
) -> None:
    url, *version = repo_url.split(PLUGIN_SPEC_SEP)
    plugin_name = _parse_plugin_name_from_git_url(url)

    install_url = f"git+{repo_url}"
    install_proc = _install_plugin_from_url_nocheck(install_url)
    if install_proc.returncode != 0:
        raise plug.PlugError(f"could not install plugin from {repo_url}")

    install_info = dict(name=url, version=repo_url)
    installed_plugins[plugin_name] = install_info
    disthelpers.write_installed_plugins(installed_plugins)
    plug.echo(f"Installed {plugin_name} from {repo_url}")
Ejemplo n.º 20
0
def _extract_expected_issues(repos_and_issues, repo_names,
                             allow_missing) -> List[Tuple[str, plug.Issue]]:
    expected_repo_names = set(repo_names)
    expected_repos_and_issues = [(repo_name, issue)
                                 for repo_name, issue in repos_and_issues
                                 if repo_name in expected_repo_names]
    missing_repos = expected_repo_names - set(
        (repo_name for repo_name, _ in expected_repos_and_issues))
    if missing_repos:
        msg = "Missing issues for: " + ", ".join(missing_repos)
        if allow_missing:
            plug.log.warning(msg)
        else:
            raise plug.PlugError(msg)

    return expected_repos_and_issues
Ejemplo n.º 21
0
    def command(self) -> None:
        """Upgrade RepoBee to the latest version."""
        plug.echo(f"Upgrading RepoBee from v{_installed_version()}...")
        repobee_requirement = f"repobee{self.version_spec or ''}"

        upgrade = disthelpers.pip(
            "install",
            repobee_requirement,
            upgrade=True,
            no_cache=True,
            force_reinstall=True,
        )
        if upgrade.returncode != 0:
            raise plug.PlugError("failed to upgrade RepoBee")

        plug.echo(f"RepoBee succesfully upgraded to v{_installed_version()}!")
Ejemplo n.º 22
0
def check_repo_state(repo_root) -> Optional[str]:
    try:
        repo = git.Repo(repo_root)
    except git.InvalidGitRepositoryError as exc:
        raise plug.PlugError(f"Not a git repository: '{repo_root}'") from exc

    message = ""
    help_message = "\n\nUse --force to ingore this warning and sanitize anyway"

    if repo.head.commit.diff():
        message = "There are uncommitted staged files in the repo"
    if repo.untracked_files:
        message = "There are untracked files in the repo"
    if repo.index.diff(None):
        message = "There are uncommitted unstaged files in the repo"

    return message + help_message if message else None
Ejemplo n.º 23
0
    def from_format(cls, format_str: str):
        r"""Build a GradeSpec tuple from a format string. The format string should
        be on the following form:

        ``<PRIORITY>:<SYMBOL>:<REGEX>``

        The expression must match the regex (\d+):([A-Za-z\d]+):(.+)

        <PRIORITY> is a positive integer value specifying how important the
        grade is. If multiple grading issues are found in the same repository,
        the one with the lowest priority is reported.

        <SYMBOL> is one or more characters specifying how the grade is
        represented in the CSV grade sheet. Only characters matching the regex
        [A-Za-z0-9] are accepted.

        <REGEX> is any valid regex to match against issue titles.

        For example, the format string "P:1:[Pp]ass" will specifies a grade
        spec with symbol P, priority 1 (the lowest possible priority) and will
        match the titles "Pass" and "pass".

        Args:
            format_str: A grade spec format string as defined above.
        Returns:
            A GradeSpec.
        """
        pattern = r"(\d+):([A-Za-z\d]+):(.+)"
        match = re.match(pattern, format_str)
        if not match:
            raise plug.PlugError(
                "invalid format string: {}".format(format_str))
        priority_str, symbol, regex = match.groups()
        priority = int(priority_str)
        return super().__new__(cls,
                               symbol=symbol,
                               priority=priority,
                               regex=regex)
Ejemplo n.º 24
0
def _install_local_plugin(plugin_path: pathlib.Path, installed_plugins: dict):
    install_info: Dict[str, Any] = dict(version="local", path=str(plugin_path))

    if plugin_path.is_dir():
        if not plugin_path.name.startswith("repobee-"):
            raise plug.PlugError(
                "RepoBee plugin package names must be prefixed with "
                "'repobee-'")

        disthelpers.pip(
            "install",
            "-e",
            str(plugin_path),
            f"repobee=={__version__}",
            upgrade=True,
        )
        ident = plugin_path.name[len("repobee-"):]
    else:
        ident = str(plugin_path)
        install_info["single_file"] = True

    installed_plugins[ident] = install_info
    disthelpers.write_installed_plugins(installed_plugins)
    plug.echo(f"Installed {ident}")
Ejemplo n.º 25
0
    def config_hook(self, config_parser: configparser.ConfigParser) -> None:
        """Look for hamcrest and junit paths in the config, and get the classpath.

        Args:
            config: the config parser after config has been read.
        """
        if SECTION not in config_parser:
            return
        self._hamcrest_path = config_parser.get(SECTION,
                                                "hamcrest_path",
                                                fallback=self._hamcrest_path)
        self._junit_path = config_parser.get(SECTION,
                                             "junit_path",
                                             fallback=self._junit_path)
        self._reference_tests_dir = config_parser.get(
            SECTION, "reference_tests_dir", fallback=self._reference_tests_dir)
        if "timeout" in config_parser[SECTION]:
            timeout = config_parser.get(SECTION, "timeout")
            if not timeout.isnumeric():
                raise plug.PlugError(
                    "config value timeout in section [{}] must be an integer, but was: {}"
                    .format(SECTION, timeout))
        self._timeout = int(
            config_parser.get(SECTION, "timeout", fallback=str(self._timeout)))
Ejemplo n.º 26
0
def read_results_file(results_file):
    if not results_file.is_file():
        raise plug.PlugError(f"no such file: {str(results_file)}")
    return plug.json_to_result_mapping(
        results_file.read_text(encoding=sys.getdefaultencoding())
    )
Ejemplo n.º 27
0
def _check_has_plugin_prefix(s: str) -> None:
    if not s.startswith(PLUGIN_PREFIX):
        raise plug.PlugError(
            "RepoBee plugin package names must be prefixed with "
            f"'{PLUGIN_PREFIX}'"
        )
Ejemplo n.º 28
0
def try_register_plugin(plugin_module: ModuleType,
                        *plugin_classes: List[type]) -> None:
    """Attempt to register a plugin module and then immediately unregister it.

    .. important::
        This is a convenience method for sanity checking plugins, and should
        only be called in test suites. It's not for production use.

    This convenience method can be used to sanity check plugins by registering
    them with RepoBee. If they have incorrectly defined hooks, this will be
    discovered only when registering.

    As an example, assume that we have a plugin module with a single (useless)
    plugin class in it, like this:

    .. code-block:: python
        :caption: useless.py

        import repobee_plug as plug

        class Useless(plug.Plugin):
            \"\"\"This plugin does nothing!\"\"\"

    We want to make sure that both the ``useless`` module and the ``Useless``
    plugin class are registered correctly, and for that we can write some
    simple code like this.

    .. code-block:: python
        :caption: Example test case to check registering

        import repobee
        # assuming that useless is defined in the external plugin
        # repobee_useless
        from repobee_useless import useless

        def test_register_useless_plugin():
            repobee.try_register_plugin(useless, useless.Useless)

    Args:
        plugin_module: A plugin module.
        plugin_classes: If the plugin contains any plugin classes (i.e. classes
            that extend :py:class:`repobee_plug.Plugin`), then these must be
            provided here. Otherwise, this option should not be provided.
    Raises:
        :py:class:`repobee_plug.PlugError` if the module cannot be registered,
        or if the contained plugin classes does not match
        plugin_classes.
    """
    expected_plugin_classes = set(plugin_classes or [])
    newly_registered = register_plugins([plugin_module])
    for reg in newly_registered:
        plug.manager.unregister(reg)

    registered_modules = [
        reg for reg in newly_registered if isinstance(reg, ModuleType)
    ]
    registered_classes = {
        cl.__class__
        for cl in newly_registered if cl not in registered_modules
    }

    assert len(registered_modules) == 1, "Module was not registered"
    if expected_plugin_classes != registered_classes:
        raise plug.PlugError(
            f"Expected plugin classes {expected_plugin_classes}, "
            f"got {registered_classes}")
Ejemplo n.º 29
0
def _install_plugin(name: str, version: str, plugins: dict) -> None:
    install_url = f"git+{plugins[name]['url']}@{version}"
    install_proc = _install_plugin_from_url_nocheck(install_url)
    if install_proc.returncode != 0:
        raise plug.PlugError(f"could not install {name} {version}")
Ejemplo n.º 30
0
def _to_student_team(name: str, data: dict) -> plug.StudentTeam:
    if _MEMBERS_KEY not in data:
        raise plug.PlugError(f"Missing members mapping for '{name}'")
    return plug.StudentTeam(name=name, members=data[_MEMBERS_KEY])