<commit_before># coding: utf-8
"""Implementation of `SSHFS`.
"""
from __future__ import unicode_literals
from __future__ import absolute_import

import itertools
import os
import stat
import socket
import sys

import six
import paramiko
from property_cached import threaded_cached_property as cached_property

from .. import errors
from ..base import FS
from ..info import Info
from ..enums import ResourceType
from ..path import basename, dirname
from ..permissions import Permissions
from ..osfs import OSFS
from ..mode import Mode

from .file import SSHFile
from .error_tools import convert_sshfs_errors


class SSHFS(FS):
    """A SSH filesystem using SFTP.

    Arguments:
        host (str): a SSH host or IP adress, e.g. ``shell.openshells.net``
        user (str): the username to connect with (defaults to the current user)
        passwd (str): Password for the server, or ``None`` for passwordless
            authentification. If given, it will be discarded immediately after
            establishing the connection.
        pkey (paramiko.PKey): a private key or a list of private key  to use.
            If ``None`` is supplied, the SSH Agent will be used to look for
            keys.
        port (int): Port number (defaults to 22).
        keepalive (int): the number of seconds after which a keep-alive message
            is sent (set to 0 to disable keepalive, default is 10).
        compress (bool): set to ``True`` to compress the messages (disable by
            default).

    Raises:
        fs.errors.CreateFailed: when the filesystem could not be created. The
            source exception is stored as the ``exc`` attribute of the
            ``CreateFailed`` error.
    """

    _meta = {
        'case_insensitive': False,
        'invalid_path_chars': '\0',
        'network': True,
        'read_only': False,
        'supports_rename': True,
        'thread_safe': sys.version_info[:2] > (3, 4),
        'unicode_paths': True,
        'virtual': False,
    }

    @staticmethod
    def _get_ssh_config(config_path='~/.ssh/config'):
        """Extract the configuration located at ``config_path``.

        Returns:
            paramiko.SSHConfig: the configuration instance.
        """
        ssh_config = paramiko.SSHConfig()
        try:
            with open(os.path.realpath(os.path.expanduser(config_path))) as f:
                ssh_config.parse(f)
        except IOError:
            pass
        return ssh_config

    def __init__(self, host, user=None, passwd=<PASSWORD>, pkey=None, timeout=10,
                 port=22, keepalive=10, compress=False,
                 config_path='~/.ssh/config', **kwargs):  # noqa: D102
        super(SSHFS, self).__init__()

        # Attempt to get a configuration for the given host
        ssh_config = self._get_ssh_config(config_path)
        config = ssh_config.lookup(host)
        pkey = config.get('identityfile') or pkey
        # Extract the given info
        pkey, keyfile = (pkey, None) \
            if isinstance(pkey, paramiko.PKey) else (None, pkey)
        self._user = user = user or config.get('user')
        self._host = host = config.get('hostname')
        self._port = port = int(config.get('port', port))
        self._client = client = paramiko.SSHClient()
        self._timeout = timeout

        try:
            # TODO: add more options
            client.load_system_host_keys()
            client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            argdict = {
                "pkey": pkey,
                "key_filename": keyfile,
                "look_for_keys": True if (pkey and keyfile) is None else False,
                "compress": compress,
                "timeout": timeout
            }

            argdict.update(kwargs)

            client.connect(
                socket.gethostbyname(host), port, user, passwd,
                **argdict
            )

            if keepalive > 0:
                client.get_transport().set_keepalive(keepalive)
            self._sftp = client.open_sftp()

        except (paramiko.ssh_exception.SSHException,            # protocol errors
                paramiko.ssh_exception.NoValidConnectionsError,  # connexion errors
                socket.gaierror, socket.timeout) as e:          # TCP errors

            message = "Unable to create filesystem: {}".format(e)
            raise errors.CreateFailed(message)

    if six.PY2:

        def close(self):  # noqa: D102
            self._client.close()
            super(SSHFS, self).close()

    else:

        def close(self): # noqa: D102
            self._client.close()
            super().close()

    def getinfo(self, path, namespaces=None):  # noqa: D102
        self.check()
        namespaces = namespaces or ()
        _path = self.validatepath(path)

        with convert_sshfs_errors('getinfo', path):
            _stat = self._sftp.stat(_path)
            info = self._make_raw_info(basename(_path), _stat, namespaces)

            if "lstat" in namespaces:
                _lstat = self._sftp.lstat(_path)
                info["lstat"] = {
                    k: getattr(_lstat, k)
                    for k in dir(_lstat)
                    if k.startswith("st_")
                }

            return Info(info)

    def geturl(self, path, purpose='download'):  # noqa: D102
        _path = self.validatepath(path)
        if purpose != 'download':
            raise errors.NoURL(path, purpose)
        return "ssh://{}@{}:{}{}".format(
            self._user, self._host, self._port, _path)

    def listdir(self, path):  # noqa: D102
        self.check()
        _path = self.validatepath(path)

        _type = self.gettype(_path)<commit_msg>Update `SSHFS.getinfo` and add `SSHFS.islink` to be consistent with `OSFS` behaviour<commit_after># coding: utf-8
"""Implementation of `SSHFS`.
"""
from __future__ import unicode_literals
from __future__ import absolute_import

import itertools
import os
import stat
import socket
import sys

import six
import paramiko
from property_cached import threaded_cached_property as cached_property

from .. import errors
from ..base import FS
from ..info import Info
from ..enums import ResourceType
from ..path import basename, dirname
from ..permissions import Permissions
from ..osfs import OSFS
from ..mode import Mode

from .file import SSHFile
from .error_tools import convert_sshfs_errors


class SSHFS(FS):
    """A SSH filesystem using SFTP.

    Arguments:
        host (str): a SSH host or IP adress, e.g. ``shell.openshells.net``
        user (str): the username to connect with (defaults to the current user)
        passwd (str): Password for the server, or ``None`` for passwordless
            authentification. If given, it will be discarded immediately after
            establishing the connection.
        pkey (paramiko.PKey): a private key or a list of private key  to use.
            If ``None`` is supplied, the SSH Agent will be used to look for
            keys.
        port (int): Port number (defaults to 22).
        keepalive (int): the number of seconds after which a keep-alive message
            is sent (set to 0 to disable keepalive, default is 10).
        compress (bool): set to ``True`` to compress the messages (disable by
            default).

    Raises:
        fs.errors.CreateFailed: when the filesystem could not be created. The
            source exception is stored as the ``exc`` attribute of the
            ``CreateFailed`` error.
    """

    _meta = {
        'case_insensitive': False,
        'invalid_path_chars': '\0',
        'network': True,
        'read_only': False,
        'supports_rename': True,
        'thread_safe': sys.version_info[:2] > (3, 4),
        'unicode_paths': True,
        'virtual': False,
    }

    @staticmethod
    def _get_ssh_config(config_path='~/.ssh/config'):
        """Extract the configuration located at ``config_path``.

        Returns:
            paramiko.SSHConfig: the configuration instance.
        """
        ssh_config = paramiko.SSHConfig()
        try:
            with open(os.path.realpath(os.path.expanduser(config_path))) as f:
                ssh_config.parse(f)
        except IOError:
            pass
        return ssh_config

    def __init__(self, host, user=None, passwd=None, pkey=None, timeout=10,
                 port=22, keepalive=10, compress=False,
                 config_path='~/.ssh/config', **kwargs):  # noqa: D102
        super(SSHFS, self).__init__()

        # Attempt to get a configuration for the given host
        ssh_config = self._get_ssh_config(config_path)
        config = ssh_config.lookup(host)
        pkey = config.get('identityfile') or pkey
        # Extract the given info
        pkey, keyfile = (pkey, None) \
            if isinstance(pkey, paramiko.PKey) else (None, pkey)
        self._user = user = user or config.get('user')
        self._host = host = config.get('hostname')
        self._port = port = int(config.get('port', port))
        self._client = client = paramiko.SSHClient()
        self._timeout = timeout

        try:
            # TODO: add more options
            client.load_system_host_keys()
            client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            argdict = {
                "pkey": pkey,
                "key_filename": keyfile,
                "look_for_keys": True if (pkey and keyfile) is None else False,
                "compress": compress,
                "timeout": timeout
            }

            argdict.update(kwargs)

            client.connect(
                socket.gethostbyname(host), port, user, passwd,
                **argdict
            )

            if keepalive > 0:
                client.get_transport().set_keepalive(keepalive)
            self._sftp = client.open_sftp()

        except (paramiko.ssh_exception.SSHException,            # protocol errors
                paramiko.ssh_exception.NoValidConnectionsError,  # connexion errors
                socket.gaierror, socket.timeout) as e:          # TCP errors

            message = "Unable to create filesystem: {}".format(e)
            raise errors.CreateFailed(message)

    if six.PY2:

        def close(self):  # noqa: D102
            self._client.close()
            super(SSHFS, self).close()

    else:

        def close(self): # noqa: D102
            self._client.close()
            super().close()

    def getinfo(self, path, namespaces=None):  # noqa: D102
        self.check()
        namespaces = namespaces or ()
        _path = self.validatepath(path)

        with convert_sshfs_errors('getinfo', path):
            _stat = self._sftp.stat(_path)
            info = self._make_raw_info(basename(_path), _stat, namespaces)

            if "lstat" in namespaces or "link" in namespaces:
                _lstat = self._sftp.lstat(_path)
            if "lstat" in namespaces:
                info["lstat"] = {
                    k: getattr(_lstat, k)
                    for k in dir(_lstat)
                    if k.startswith("st_")
                }
            if "link" in namespaces:
                if OSFS._get_type_from_stat(_lstat) == ResourceType.symlink:
                    target = self._sftp.readlink(_path)
                    info["link"] = {"target": target}
                else:
                    info["link"] = {"target": None}

            return Info(info)

    def geturl(self, path, purpose='download'):  # noqa: D102
        _path = self.validatepath(path)
        if purpose != 'download':
            raise errors.NoURL(path, purpose)
        return "ssh://{}@{}:{}{}".format(
            self._user, self._host, self._port, _path)

    def islink(self, path):  # noqa: D102
        return self.getinfo(path, namespaces=["link"]).is_link

    def listdir(self, path):  # noqa: D102
        self.check()
        _path = self.validatepath(path)

        _type = self.gettype(_path)
{'fraction_non_alphanumeric': 0.10072968212698147, 'fraction_numerical': 0.006290363163633314, 'mean_word_length': 2.0371879775853285, 'pattern_counts': {'":': 12, '<': 4, '<?xml version=': 0, '>': 8, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3051263', 'n_tokens_mistral': 3510, 'n_tokens_neox': 3321, 'n_words': 983}