<commit_before>"""This module exports the PythonLinter subclass of Linter."""

from functools import lru_cache
import os
import re
import subprocess

import sublime
from .. import linter, persist, util


class PythonLinter(linter.Linter):
    """(New) Python Linter [WIP]."""

    comment_re = r'\s*#'

    @classmethod
    @lru_cache(maxsize=None)
    def can_lint(cls, syntax):
        """Determine optimistically if the linter can handle the provided syntax."""
        can = False
        syntax = syntax.lower()

        if cls.syntax:
            if isinstance(cls.syntax, (tuple, list)):
                can = syntax in cls.syntax
            elif cls.syntax == '*':
                can = True
            elif isinstance(cls.syntax, str):
                can = syntax == cls.syntax
            else:
                can = cls.syntax.match(syntax) is not None

        return can

    def context_sensitive_executable_path(self, cmd):
        """Try to find an executable for a given cmd."""
        settings = self.get_view_settings()

        # If the user explicitly set an executable, it takes precedence.
        # We expand environment variables. E.g. a user could have a project
        # structure where a virtual environment is always located within
        # the project structure. She could then simply specify
        # `${project_path}/venv/bin/flake8`. Note that setting `@python`
        # to a path will have a similar effect.
        executable = settings.get('executable', '')
        if executable:
            executable = expand_variables(executable)

            persist.debug(
                "{}: wanted executable is '{}'".format(self.name, executable)
            )

            if util.can_exec(executable):
                return True, executable

            persist.printf(
                "ERROR: {} deactivated, cannot locate '{}' "
                .format(self.name, executable)
            )
            # no fallback, the user specified something, so we err
            return True, None

        # `@python` can be number or a string. If it is a string it should
        # point to a python environment, NOT a python binary.
        # We expand environment variables. E.g. a user could have a project
        # structure where virtual envs are located always like such
        # `some/where/venvs/${project_base_name}` or she has the venv
        # contained in the project dir `${project_path}/venv`. She then
        # could edit the global settings once and can be sure that always the
        # right linter installed in the virtual environment gets executed.
        python = settings.get('@python', None)
        if isinstance(python, str):
            python = expand_variables(python)

        persist.debug(
            "{}: wanted @python is '{}'".format(self.name, python)
        )

        cmd_name = cmd[0] if isinstance(cmd, (list, tuple)) else cmd

        if python:
            if isinstance(python, str):
                executable = find_script_by_python_env(
                    python, cmd_name
                )
                if not executable:
                    persist.printf(
                        "WARNING: {} deactivated, cannot locate '{}' "
                        "for given @python '{}'"
                        .format(self.name, cmd_name, python)
                    )
                    # Do not fallback, user specified something we didn't find
                    return True, None

                return True, executable

            else:
                executable = find_script_by_python_version(
                    cmd_name, str(python)
                )

                if executable is None:
                    persist.printf(
                        "WARNING: {} deactivated, cannot locate '{}' "
                        "for given @python '{}'"
                        .format(self.name, cmd_name, python)
                    )
                    return True, None

                persist.debug(
                    "{}: Using {} for given @python '{}'"
                    .format(self.name, executable, python)
                )
                return True, executable

        # If we're here the user didn't specify anything. This is the default
        # experience. So we kick in some 'magic'
        chdir = self.get_chdir(settings)
        executable = ask_pipenv(cmd[0], chdir)
        if executable:
            persist.debug(
                "{}: Using {} according to 'pipenv'"
                .format(self.name, executable)
            )
            return True, executable

        # Should we try a `pyenv which` as well? Problem: I don't have it,
        # it's MacOS only.

        persist.debug(
            "{}: trying to use globally installed {}"
            .format(self.name, cmd_name)
        )
        # fallback, similiar to a which(cmd)
        executable = util.which(cmd_name)
        if executable is None:
            persist.printf(
                "WARNING: cannot locate '{}'. Fill in the '@python' or "
                "'executable' setting."
                .format(self.name)
            )
        return True, executable


def find_python_version(version):  # type: Str
    """Return python binaries on PATH matching a specific version."""
    requested_version = extract_major_minor_version(version)
    for python in util.find_executables('python'):
        python_version = get_python_version(python)
        if version_fulfills_request(python_version, requested_version):
            yield python

    return None


@lru_cache(maxsize=None)
def find_script_by_python_version(script_name, version):
    """Return full path to a script, given just a python version."""
    # They can be multiple matching pythons. We try to find a python with
    # its complete environment, not just a symbolic link or so.
    for python in find_python_version(version):
        python_env = os.path.dirname(python)
        script_path = find_script_by_python_env(python_env, script_name)
        if script_path:
            return script_path

    return None


@lru_cache(maxsize=None)
def find_script_by_python_env(python_env_path, script):
    """Return full path to a script, given a python environment base dir."""
    posix = sublime.platform() in ('osx', 'linux')

    if posix:
        full_path = os.path.join(python_env_path, 'bin', script)
    else:
        full_path = os.path.join(python_env_path, 'Scripts', script + '.exe')

    persist.printf("trying {}".format(full_path))
    if os.path.exists(full_path):
        return full_path

    return None


def expand_variables(string):
    """Expand typical sublime variables in the given string."""
    window = sublime.active_window()<commit_msg>Expand user and environment variables in PythonLinter<commit_after>"""This module exports the PythonLinter subclass of Linter."""

from functools import lru_cache
import os
import re
import subprocess

import sublime
from .. import linter, persist, util


class PythonLinter(linter.Linter):
    """(New) Python Linter [WIP]."""

    comment_re = r'\s*#'

    @classmethod
    @lru_cache(maxsize=None)
    def can_lint(cls, syntax):
        """Determine optimistically if the linter can handle the provided syntax."""
        can = False
        syntax = syntax.lower()

        if cls.syntax:
            if isinstance(cls.syntax, (tuple, list)):
                can = syntax in cls.syntax
            elif cls.syntax == '*':
                can = True
            elif isinstance(cls.syntax, str):
                can = syntax == cls.syntax
            else:
                can = cls.syntax.match(syntax) is not None

        return can

    def context_sensitive_executable_path(self, cmd):
        """Try to find an executable for a given cmd."""
        settings = self.get_view_settings()

        # If the user explicitly set an executable, it takes precedence.
        # We expand environment variables. E.g. a user could have a project
        # structure where a virtual environment is always located within
        # the project structure. She could then simply specify
        # `${project_path}/venv/bin/flake8`. Note that setting `@python`
        # to a path will have a similar effect.
        executable = settings.get('executable', '')
        if executable:
            executable = expand_variables(executable)

            persist.debug(
                "{}: wanted executable is '{}'".format(self.name, executable)
            )

            if util.can_exec(executable):
                return True, executable

            persist.printf(
                "ERROR: {} deactivated, cannot locate '{}' "
                .format(self.name, executable)
            )
            # no fallback, the user specified something, so we err
            return True, None

        # `@python` can be number or a string. If it is a string it should
        # point to a python environment, NOT a python binary.
        # We expand environment variables. E.g. a user could have a project
        # structure where virtual envs are located always like such
        # `some/where/venvs/${project_base_name}` or she has the venv
        # contained in the project dir `${project_path}/venv`. She then
        # could edit the global settings once and can be sure that always the
        # right linter installed in the virtual environment gets executed.
        python = settings.get('@python', None)
        if isinstance(python, str):
            python = expand_variables(python)

        persist.debug(
            "{}: wanted @python is '{}'".format(self.name, python)
        )

        cmd_name = cmd[0] if isinstance(cmd, (list, tuple)) else cmd

        if python:
            if isinstance(python, str):
                executable = find_script_by_python_env(
                    python, cmd_name
                )
                if not executable:
                    persist.printf(
                        "WARNING: {} deactivated, cannot locate '{}' "
                        "for given @python '{}'"
                        .format(self.name, cmd_name, python)
                    )
                    # Do not fallback, user specified something we didn't find
                    return True, None

                return True, executable

            else:
                executable = find_script_by_python_version(
                    cmd_name, str(python)
                )

                if executable is None:
                    persist.printf(
                        "WARNING: {} deactivated, cannot locate '{}' "
                        "for given @python '{}'"
                        .format(self.name, cmd_name, python)
                    )
                    return True, None

                persist.debug(
                    "{}: Using {} for given @python '{}'"
                    .format(self.name, executable, python)
                )
                return True, executable

        # If we're here the user didn't specify anything. This is the default
        # experience. So we kick in some 'magic'
        chdir = self.get_chdir(settings)
        executable = ask_pipenv(cmd[0], chdir)
        if executable:
            persist.debug(
                "{}: Using {} according to 'pipenv'"
                .format(self.name, executable)
            )
            return True, executable

        # Should we try a `pyenv which` as well? Problem: I don't have it,
        # it's MacOS only.

        persist.debug(
            "{}: trying to use globally installed {}"
            .format(self.name, cmd_name)
        )
        # fallback, similiar to a which(cmd)
        executable = util.which(cmd_name)
        if executable is None:
            persist.printf(
                "WARNING: cannot locate '{}'. Fill in the '@python' or "
                "'executable' setting."
                .format(self.name)
            )
        return True, executable


def find_python_version(version):  # type: Str
    """Return python binaries on PATH matching a specific version."""
    requested_version = extract_major_minor_version(version)
    for python in util.find_executables('python'):
        python_version = get_python_version(python)
        if version_fulfills_request(python_version, requested_version):
            yield python

    return None


@lru_cache(maxsize=None)
def find_script_by_python_version(script_name, version):
    """Return full path to a script, given just a python version."""
    # They can be multiple matching pythons. We try to find a python with
    # its complete environment, not just a symbolic link or so.
    for python in find_python_version(version):
        python_env = os.path.dirname(python)
        script_path = find_script_by_python_env(python_env, script_name)
        if script_path:
            return script_path

    return None


@lru_cache(maxsize=None)
def find_script_by_python_env(python_env_path, script):
    """Return full path to a script, given a python environment base dir."""
    posix = sublime.platform() in ('osx', 'linux')

    if posix:
        full_path = os.path.join(python_env_path, 'bin', script)
    else:
        full_path = os.path.join(python_env_path, 'Scripts', script + '.exe')

    persist.printf("trying {}".format(full_path))
    if os.path.exists(full_path):
        return full_path

    return None


def expand_variables(string):
    """Expand environment, user, and sublime text variables in the given string.

    User variables:
    https://docs.python.org/3/library/os.path.html#os.path.expanduser

    Environment variables:
    See https://docs.python.org/3/library/os.path.html#os.path.expandvars

    Sublime Text variables:
    e.g. "packages", "platform", "file", "file_path", file_name",
    "file_base_name", "file_extension, "folder", "project", project_path",
    "project_name", "project_base_name, "project_extension".
    """
    string = os.path.expanduser(string)
    string = os.path.expandvars(string)
    window = sublime.active_window()
{'fraction_non_alphanumeric': 0.09072135583564765, 'fraction_numerical': 0.0005696788435519476, 'mean_word_length': 1.9473242392444912, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2866476', 'n_tokens_mistral': 3833, 'n_tokens_neox': 3600, 'n_words': 1192}