diff --git a/Pipfile.lock b/Pipfile.lock index 009c355..cfa8175 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -79,24 +79,8 @@ "sha256:0bada4c2f8a43d533e4ecb7a12214d9420e66eb206d54bf2d682581ca4b80848", "sha256:8fde5f188da2d593bd5bc0be98d9abc46c95bb8a9dde93429570192ee6cc2d4a" ], - "markers": "python_version < '3.2'", - "version": "==1.6.1" - }, - "backports.shutil-get-terminal-size": { - "hashes": [ - "sha256:0975ba55054c15e346944b38956a4c9cbee9009391e41b86c68990effb8c1f64", - "sha256:713e7a8228ae80341c70586d1cc0a8caa5207346927e23d09dcbcaf18eadec80" - ], "markers": "python_version == '2.7'", - "version": "==1.0.0" - }, - "backports.weakref": { - "hashes": [ - "sha256:81bc9b51c0abc58edc76aefbbc68c62a787918ffe943a37947e162c3f8e19e82", - "sha256:bc4170a29915f8b22c9e7c4939701859650f2eb84184aee80da329ac0b9825c2" - ], - "markers": "python_version == '2.7'", - "version": "==1.0.post1" + "version": "==1.6.1" }, "black": { "hashes": [ @@ -222,14 +206,6 @@ ], "version": "==0.4.2" }, - "colorama": { - "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.4.3" - }, "configparser": { "hashes": [ "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c", @@ -284,13 +260,6 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", "version": "==4.5.4" }, - "crayons": { - "hashes": [ - "sha256:50e5fa729d313e2c607ae8bf7b53bb487652e10bd8e7a1e08c4bc8bf62755ffc", - "sha256:8c9e4a3a607bc10e9a9140d496ecd16c6805088dd16c852c378f1f1d5db7aeb6" - ], - "version": "==0.3.0" - }, "cryptography": { "hashes": [ "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", @@ -674,7 +643,7 @@ "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db", "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868" ], - "markers": "python_version < '3'", + "markers": "python_version < '3.5'", "version": "==2.3.5" }, "pathspec": { @@ -847,29 +816,29 @@ }, "regex": { "hashes": [ - "sha256:032fdcc03406e1a6485ec09b826eac78732943840c4b29e503b789716f051d8d", - "sha256:0e6cf1e747f383f52a0964452658c04300a9a01e8a89c55ea22813931b580aa8", - "sha256:106e25a841921d8259dcef2a42786caae35bc750fb996f830065b3dfaa67b77e", - "sha256:1768cf42a78a11dae63152685e7a1d90af7a8d71d2d4f6d2387edea53a9e0588", - "sha256:27d1bd20d334f50b7ef078eba0f0756a640fd25f5f1708d3b5bed18a5d6bced9", - "sha256:29b20f66f2e044aafba86ecf10a84e611b4667643c42baa004247f5dfef4f90b", - "sha256:4850c78b53acf664a6578bba0e9ebeaf2807bb476c14ec7e0f936f2015133cae", - "sha256:57eacd38a5ec40ed7b19a968a9d01c0d977bda55664210be713e750dd7b33540", - "sha256:724eb24b92fc5fdc1501a1b4df44a68b9c1dda171c8ef8736799e903fb100f63", - "sha256:77ae8d926f38700432807ba293d768ba9e7652df0cbe76df2843b12f80f68885", - "sha256:78b3712ec529b2a71731fbb10b907b54d9c53a17ca589b42a578bc1e9a2c82ea", - "sha256:7bbbdbada3078dc360d4692a9b28479f569db7fc7f304b668787afc9feb38ec8", - "sha256:8d9ef7f6c403e35e73b7fc3cde9f6decdc43b1cb2ff8d058c53b9084bfcb553e", - "sha256:a83049eb717ae828ced9cf607845929efcb086a001fc8af93ff15c50012a5716", - "sha256:adc35d38952e688535980ae2109cad3a109520033642e759f987cf47fe278aa1", - "sha256:c29a77ad4463f71a506515d9ec3a899ed026b4b015bf43245c919ff36275444b", - "sha256:cfd31b3300fefa5eecb2fe596c6dee1b91b3a05ece9d5cfd2631afebf6c6fadd", - "sha256:d3ee0b035816e0520fac928de31b6572106f0d75597f6fa3206969a02baba06f", - "sha256:d508875793efdf6bab3d47850df8f40d4040ae9928d9d80864c1768d6aeaf8e3", - "sha256:ef0b828a7e22e58e06a1cceddba7b4665c6af8afeb22a0d8083001330572c147", - "sha256:faad39fdbe2c2ccda9846cd21581063086330efafa47d87afea4073a08128656" - ], - "version": "==2019.12.20" + "sha256:08047f4b31254489316b489c24983d72c0b9d520da084b8c624f45891a9c6da2", + "sha256:08d042155592c24cbdb81158a99aeeded4493381a1aba5eba9def6d29961042c", + "sha256:13901ac914de7a7e58a92f99c71415e268e88ac4be8b389d8360c38e64b2f1c5", + "sha256:15b6f7e10f764c5162242a7db89da51218a38299415ba5e70f235a6a83c53b94", + "sha256:46d01bb4139e7051470037f8b9a5b90c48cb77a3d307c2621bf3791bfae4d9d8", + "sha256:52814a8423d52a7e0f070dbb79f7bdfce5221992b881f83bad69f8daf4b831c3", + "sha256:6d999447f77b1b638ea620bde466b958144af90ac2e9b1f23b98a79ced14ce3f", + "sha256:7391eeee49bb3ce895ca43479eaca810f0c2608556711fa02a82075768f81a37", + "sha256:79530d60a8644f72f78834c01a2d70a60be110e2f4a0a612b78da23ef60c2730", + "sha256:841056961d441f05b949d9003e7f2b5d51a11dd52d8bd7c0a5325943b6a0ea6b", + "sha256:895f95344182b4ecb84044910e62ad33ca63a7e7b447c7ba858d24e9f1aad939", + "sha256:93e797cf16e07b315413d1157b5ce7a7c2b28b2b95768e25c0ccd290443661ad", + "sha256:a4677dc8245f1127b70fa79fb7f15a61eae0fee36ae15cbbe017207485fe9a5c", + "sha256:b2faf1dce478c0ca1c92575bdc48b7afdce3a887a02afb6342fae476af41bbe2", + "sha256:bcd9bcba67ae8d1e1b21426ea7995f7ca08260bea601ba15e13e5ca8588208ef", + "sha256:d47a89e6029852c88fff859dbc9a11dcec820413b4c2510e80ced1c99c3e79ea", + "sha256:dd69d165bee099b02d122d1e0dd55a85ebf9a65493dcd17124b628db9edfc833", + "sha256:e77f64a3ae8b9a555e170a3908748b4e2ccd0c58f8385f328baf8fc70f9ea497", + "sha256:ec75e8baa576aed6065b615a8f8e91a05e42b492b24ffd16cbb075ad62fb9185", + "sha256:ed75b64c6694bbe840b3340191b2039f633fd1ec6fc567454e47d7326eda557f", + "sha256:ef85a6a15342559bed737dc16dfb1545dc043ca5bf5bce6bff4830f0e7a74395" + ], + "version": "==2020.1.7" }, "requests": { "hashes": [ @@ -1074,17 +1043,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==16.7.9" }, - "vistir": { - "extras": [ - "spinner" - ], - "hashes": [ - "sha256:2166e3148a67c438c9e3edbba0cde153d42dec6e3bf5d8f4624feb27686c0990", - "sha256:3a0529b4b6c2e842fd19b5ceaa95b6c9201321314825c110406d4af3331a0709" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.4.3" - }, "watchdog": { "hashes": [ "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d" @@ -1113,13 +1071,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.33.6" }, - "yaspin": { - "hashes": [ - "sha256:0ee4668936d0053de752c9a4963929faa3a832bd0ba823877d27855592dc80aa", - "sha256:5a938bdc7bab353fd8942d0619d56c6b5159a80997dc1c387a479b39e6dc9391" - ], - "version": "==0.15.0" - }, "zipp": { "hashes": [ "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", diff --git a/news/78.feature.rst b/news/78.feature.rst new file mode 100644 index 0000000..357e47f --- /dev/null +++ b/news/78.feature.rst @@ -0,0 +1 @@ +Reduced dependencies by removing ``vistir``,, ``crayons`` and intermediate calls. diff --git a/setup.cfg b/setup.cfg index fddc022..79ad1dc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,10 +43,10 @@ install_requires = attrs cached-property click - crayons six packaging - vistir[spinner]>=0.4 + backports.functools_lru_cache;python_version=="2.7" + pathlib2;python_version<"3.5" [options.packages.find] where = src diff --git a/src/pythonfinder/cli.py b/src/pythonfinder/cli.py index eb2e603..fc86abd 100644 --- a/src/pythonfinder/cli.py +++ b/src/pythonfinder/cli.py @@ -1,10 +1,7 @@ # -*- coding=utf-8 -*- from __future__ import absolute_import, print_function, unicode_literals -import sys - import click -import crayons from . import __version__ from .pythonfinder import Finder @@ -32,10 +29,11 @@ def cli( if version: click.echo( "{0} version {1}".format( - crayons.white("PythonFinder", bold=True), crayons.yellow(__version__) + click.style("PythonFinder", fg="white", bold=True), + click.style(str(__version__), fg="yellow") ) ) - sys.exit(0) + ctx.exit() finder = Finder(ignore_unsupported=ignore_unsupported) if findall: versions = [v for v in finder.find_all_python_versions()] @@ -54,7 +52,7 @@ def cli( ), fg="yellow", ) - sys.exit(0) + ctx.exit() else: click.secho( "ERROR: No valid python versions found! Check your path and try again.", @@ -78,22 +76,22 @@ def cli( ), fg="yellow", ) - sys.exit(0) + ctx.exit() else: click.secho("Failed to find matching executable...", fg="yellow") - sys.exit(1) + ctx.exit(1) elif which: found = finder.system_path.which(which.strip()) if found: click.secho("Found Executable: {0}".format(found), fg="white") - sys.exit(0) + ctx.exit() else: click.secho("Failed to find matching executable...", fg="yellow") - sys.exit(1) + ctx.exit(1) else: click.echo("Please provide a command", color="red") - sys.exit(1) - sys.exit() + ctx.exit(1) + ctx.exit() if __name__ == "__main__": diff --git a/src/pythonfinder/compat.py b/src/pythonfinder/compat.py new file mode 100644 index 0000000..6fb4542 --- /dev/null +++ b/src/pythonfinder/compat.py @@ -0,0 +1,42 @@ +# -*- coding=utf-8 -*- +import sys + +import six + +if sys.version_info[:2] <= (3, 4): + from pathlib2 import Path # type: ignore # noqa +else: + from pathlib import Path + +if six.PY3: + from functools import lru_cache + from builtins import TimeoutError +else: + from backports.functools_lru_cache import lru_cache # type: ignore # noqa + + class TimeoutError(OSError): + pass + + +def getpreferredencoding(): + import locale + # Borrowed from Invoke + # (see https://github.com/pyinvoke/invoke/blob/93af29d/invoke/runners.py#L881) + _encoding = locale.getpreferredencoding(False) + if six.PY2 and not sys.platform == "win32": + _default_encoding = locale.getdefaultlocale()[1] + if _default_encoding is not None: + _encoding = _default_encoding + return _encoding + + +DEFAULT_ENCODING = getpreferredencoding() + + +def fs_str(string): + """Encodes a string into the proper filesystem encoding""" + + if isinstance(string, str): + return string + assert not isinstance(string, bytes) + return string.encode(DEFAULT_ENCODING) diff --git a/src/pythonfinder/models/mixins.py b/src/pythonfinder/models/mixins.py index 4c473b1..7632711 100644 --- a/src/pythonfinder/models/mixins.py +++ b/src/pythonfinder/models/mixins.py @@ -7,8 +7,8 @@ import attr import six -from vistir.compat import fs_str +from ..compat import fs_str from ..environment import MYPY_RUNNING from ..exceptions import InvalidPythonVersion from ..utils import ( @@ -35,7 +35,7 @@ TypeVar, Type, ) - from vistir.compat import Path + from ..compat import Path # noqa BaseFinderType = TypeVar("BaseFinderType") diff --git a/src/pythonfinder/models/path.py b/src/pythonfinder/models/path.py index f46677e..b855a05 100644 --- a/src/pythonfinder/models/path.py +++ b/src/pythonfinder/models/path.py @@ -10,8 +10,7 @@ import attr import six from cached_property import cached_property -from vistir.compat import Path, fs_str -from vistir.misc import dedup +from ..compat import Path, fs_str from ..environment import ( ASDF_DATA_DIR, @@ -26,6 +25,7 @@ from ..utils import ( Iterable, Sequence, + dedup, ensure_path, expand_paths, filter_pythons, diff --git a/src/pythonfinder/models/python.py b/src/pythonfinder/models/python.py index 3c85998..619e776 100644 --- a/src/pythonfinder/models/python.py +++ b/src/pythonfinder/models/python.py @@ -10,8 +10,8 @@ import attr import six from packaging.version import Version -from vistir.compat import Path, lru_cache +from ..compat import Path, lru_cache from ..environment import ASDF_DATA_DIR, MYPY_RUNNING, PYENV_ROOT, SYSTEM_ARCH from ..exceptions import InvalidPythonVersion from ..utils import ( diff --git a/src/pythonfinder/pythonfinder.py b/src/pythonfinder/pythonfinder.py index 400a317..9673735 100644 --- a/src/pythonfinder/pythonfinder.py +++ b/src/pythonfinder/pythonfinder.py @@ -7,9 +7,9 @@ import six from click import secho -from vistir.compat import lru_cache from . import environment +from .compat import lru_cache from .exceptions import InvalidPythonVersion from .utils import Iterable, filter_pythons, version_re @@ -51,7 +51,8 @@ def __init__( :param system: bool, optional :param global_search: Whether to search the global path from os.environ, defaults to True :param global_search: bool, optional - :param ignore_unsupported: Whether to ignore unsupported python versions, if False, an error is raised, defaults to True + :param ignore_unsupported: Whether to ignore unsupported python versions, if False, an + error is raised, defaults to True :param ignore_unsupported: bool, optional :param bool sort_by_path: Whether to always sort by path :returns: a :class:`~pythonfinder.pythonfinder.Finder` object. @@ -133,8 +134,16 @@ def which(self, exe): return self.system_path.which(exe) @classmethod - def parse_major(cls, major, minor=None, patch=None, pre=None, dev=None, arch=None): - # type: (Optional[str], Optional[int], Optional[int], Optional[bool], Optional[bool], Optional[str]) -> Dict[str, Union[int, str, bool, None]] + def parse_major( + cls, + major, # type: Optional[str] + minor=None, # type: Optional[int] + patch=None, # type: Optional[int] + pre=None, # type: Optional[bool] + dev=None, # type: Optional[bool] + arch=None, # type: Optional[str] + ): + # type: (...) -> Dict[str, Union[int, str, bool, None]] from .models import PythonVersion major_is_str = major and isinstance(major, six.string_types) @@ -289,11 +298,18 @@ def find_python_version( @lru_cache(maxsize=1024) def find_all_python_versions( - self, major=None, minor=None, patch=None, pre=None, dev=None, arch=None, name=None + self, + major=None, # type: Optional[Union[str, int]] + minor=None, # type: Optional[int] + patch=None, # type: Optional[int] + pre=None, # type: Optional[bool] + dev=None, # type: Optional[bool] + arch=None, # type: Optional[str] + name=None, # type: Optional[str] ): - # type: (Optional[Union[str, int]], Optional[int], Optional[int], Optional[bool], Optional[bool], Optional[str], Optional[str]) -> List[PathEntry] + # type: (...) -> List[PathEntry] version_sort = operator.attrgetter("as_python.version_sort") - python_version_dict = getattr(self.system_path, "python_version_dict") + python_version_dict = getattr(self.system_path, "python_version_dict", {}) if python_version_dict: paths = ( path diff --git a/src/pythonfinder/utils.py b/src/pythonfinder/utils.py index 1c190b0..8150545 100644 --- a/src/pythonfinder/utils.py +++ b/src/pythonfinder/utils.py @@ -5,14 +5,16 @@ import itertools import os import re +import subprocess +from collections import OrderedDict from fnmatch import fnmatch from threading import Timer import attr import six -import vistir from packaging.version import LegacyVersion, Version +from .compat import Path, lru_cache, TimeoutError # noqa from .environment import MYPY_RUNNING, PYENV_ROOT, SUBPROCESS_TIMEOUT from .exceptions import InvalidPythonVersion @@ -27,11 +29,6 @@ from six.moves import Sequence # type: ignore # noqa # isort:skip # fmt: on -try: - from functools import lru_cache -except ImportError: - from backports.functools_lru_cache import lru_cache # type: ignore # noqa - if MYPY_RUNNING: from typing import Any, Union, List, Callable, Set, Tuple, Dict, Optional, Iterator from attr.validators import _OptionalValidator # type: ignore @@ -98,21 +95,26 @@ def get_python_version(path): "-c", "import sys; print('.'.join([str(i) for i in sys.version_info[:3]]))", ] + subprocess_kwargs = { + "env": os.environ.copy(), + "universal_newlines": True, + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + "shell": False, + } + c = subprocess.Popen(version_cmd, **subprocess_kwargs) + timer = Timer(SUBPROCESS_TIMEOUT, c.kill) try: - c = vistir.misc.run( - version_cmd, - block=True, - nospin=True, - return_object=True, - combine_stderr=False, - write_to_stdout=False, - ) - timer = Timer(SUBPROCESS_TIMEOUT, c.kill) + out, _ = c.communicate() + except (SystemExit, KeyboardInterrupt, TimeoutError): + c.terminate() + out, _ = c.communicate() + raise except OSError: raise InvalidPythonVersion("%s is not a valid python path" % path) - if not c.out: + if not out: raise InvalidPythonVersion("%s is not a valid python path" % path) - return c.out.strip() + return out.strip() @lru_cache(maxsize=1024) @@ -190,13 +192,13 @@ def path_is_executable(path): @lru_cache(maxsize=1024) def path_is_known_executable(path): - # type: (vistir.compat.Path) -> bool + # type: (Path) -> bool """ Returns whether a given path is a known executable from known executable extensions or has the executable bit toggled. :param path: The path to the target executable. - :type path: :class:`~vistir.compat.Path` + :type path: :class:`~Path` :return: True if the path has chmod +x, or is a readable, known executable extension. :rtype: bool """ @@ -229,12 +231,12 @@ def looks_like_python(name): @lru_cache(maxsize=1024) def path_is_python(path): - # type: (vistir.compat.Path) -> bool + # type: (Path) -> bool """ Determine whether the supplied path is executable and looks like a possible path to python. :param path: The path to an executable. - :type path: :class:`~vistir.compat.Path` + :type path: :class:`~Path` :return: Whether the provided path is an executable path to python. :rtype: bool """ @@ -278,7 +280,7 @@ def path_is_pythoncore(path): @lru_cache(maxsize=1024) def ensure_path(path): - # type: (Union[vistir.compat.Path, str]) -> vistir.compat.Path + # type: (Union[Path, str]) -> Path """ Given a path (either a string or a Path object), expand variables and return a Path object. @@ -288,9 +290,9 @@ def ensure_path(path): :rtype: :class:`~pathlib.Path` """ - if isinstance(path, vistir.compat.Path): + if isinstance(path, Path): return path - path = vistir.compat.Path(os.path.expandvars(path)) + path = Path(os.path.expandvars(path)) return path.absolute() @@ -313,10 +315,10 @@ def normalize_path(path): @lru_cache(maxsize=1024) def filter_pythons(path): - # type: (Union[str, vistir.compat.Path]) -> Iterable + # type: (Union[str, Path]) -> Iterable """Return all valid pythons in a given path""" - if not isinstance(path, vistir.compat.Path): - path = vistir.compat.Path(str(path)) + if not isinstance(path, Path): + path = Path(str(path)) if not path.is_dir(): return path if path_is_python(path) else None return filter(path_is_python, path.iterdir()) @@ -377,7 +379,7 @@ def split_version_and_name( patch=None, # type: Optional[Union[str, int]] name=None, # type: Optional[str] ): - # type: (...) -> Tuple[Optional[Union[str, int]], Optional[Union[str, int]], Optional[Union[str, int]], Optional[str]] + # type: (...) -> Tuple[Optional[Union[str, int]], Optional[Union[str, int]], Optional[Union[str, int]], Optional[str]] # noqa if isinstance(major, six.string_types) and not minor and not patch: # Only proceed if this is in the format "x.y.z" or similar if major.isdigit() or (major.count(".") > 0 and major[0].isdigit()): @@ -437,3 +439,11 @@ def expand_paths(path, only_python=True): else: if path is not None and path.is_python and path.as_python is not None: yield path + + +def dedup(iterable): + # type: (Iterable) -> Iterable + """Deduplicate an iterable object like iter(set(iterable)) but + order-reserved. + """ + return iter(OrderedDict.fromkeys(iterable)) diff --git a/tests/conftest.py b/tests/conftest.py index 5227f1d..1bc54d7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,21 +1,40 @@ # -*- coding=utf-8 -*- from __future__ import absolute_import, print_function +import atexit import os import random +import shutil import stat +import subprocess import sys +import tempfile +import warnings from collections import namedtuple +from contextlib import contextmanager import click import click.testing import pytest import six -import vistir import pythonfinder -from .testutils import normalized_match, yield_versions +from .testutils import ( + cd, + create_tracked_tempdir, + normalize_path, + normalized_match, + set_write_bit, + temp_environ, + yield_versions, +) + +if sys.version_info[:2] < (3, 5): + from pathlib2 import Path +else: + from pathlib import Path + pythoninfo = namedtuple("PythonVersion", ["name", "version", "path", "arch"]) @@ -27,7 +46,7 @@ def pytest_runtest_setup(item): @pytest.fixture def pathlib_tmpdir(request, tmpdir): - yield vistir.compat.Path(str(tmpdir)) + yield Path(str(tmpdir)) try: tmpdir.remove(ignore_errors=True) except Exception: @@ -39,30 +58,30 @@ def _create_tracked_dir(): temp_args["dir"] = os.environ.get("TMPDIR", "/tmp") if temp_args["dir"] == "/": temp_args["dir"] = os.getcwd() - temp_path = vistir.path.create_tracked_tempdir(**temp_args) + temp_path = create_tracked_tempdir(**temp_args) return temp_path @pytest.fixture -def vistir_tmpdir(): +def tracked_tmpdir(): temp_path = _create_tracked_dir() - yield vistir.compat.Path(temp_path) + yield Path(temp_path) @pytest.fixture(name="create_tmpdir") -def vistir_tmpdir_factory(): +def tracked_tmpdir_factory(): def create_tmpdir(): - return vistir.compat.Path(_create_tracked_dir()) + return Path(_create_tracked_dir()) yield create_tmpdir @pytest.fixture def no_virtual_env(): - with vistir.contextmanagers.temp_environ(): + with temp_environ(): if "VIRTUAL_ENV" in os.environ["PATH"]: orig_path = os.environ["PATH"].split(os.pathsep) - venv = vistir.path.normalize_path(os.environ["VIRTUAL_ENV"]) + venv = normalize_path(os.environ["VIRTUAL_ENV"]) match = next(iter(p for p in orig_path if normalized_match(p, venv)), None) if match: orig_path.remove(match) @@ -88,12 +107,12 @@ def no_pyenv_root_envvar(monkeypatch): m.setattr(pythonfinder.environment, "PYENV_INSTALLED", False) m.setattr(pythonfinder.environment, "ASDF_INSTALLED", False) m.setattr( - pythonfinder.environment, "PYENV_ROOT", vistir.path.normalize_path("~/.pyenv") + pythonfinder.environment, "PYENV_ROOT", normalize_path("~/.pyenv") ) m.setattr( pythonfinder.environment, "ASDF_DATA_DIR", - vistir.path.normalize_path("~/.asdf"), + normalize_path("~/.asdf"), ) m.setattr( pythonfinder.environment, @@ -108,11 +127,11 @@ def isolated_envdir(create_tmpdir): runner = click.testing.CliRunner() fake_root_path = create_tmpdir() fake_root = fake_root_path.as_posix() - vistir.path.set_write_bit(fake_root) - with vistir.contextmanagers.temp_environ(), vistir.contextmanagers.cd(fake_root): + set_write_bit(fake_root) + with temp_environ(), cd(fake_root): home_dir_path = fake_root_path.joinpath("home/pythonfinder") home_dir_path.mkdir(parents=True) - home_dir = vistir.path.normalize_path(home_dir_path.as_posix()) + home_dir = normalize_path(home_dir_path.as_posix()) os.chdir(home_dir) # This is pip's isolation approach, swipe it for now for time savings if sys.platform == "win32": @@ -126,18 +145,18 @@ def isolated_envdir(create_tmpdir): ): path = os.path.join(home_dir, *sub_path.split("/")) os.environ[env_var] = path - vistir.path.mkdir_p(path) + Path(path).mkdir(exist_ok=True, parents=True) else: os.environ["HOME"] = home_dir os.environ["XDG_DATA_HOME"] = os.path.join(home_dir, ".local", "share") os.environ["XDG_CONFIG_HOME"] = os.path.join(home_dir, ".config") os.environ["XDG_CACHE_HOME"] = os.path.join(home_dir, ".cache") os.environ["XDG_RUNTIME_DIR"] = os.path.join(home_dir, ".runtime") - vistir.path.mkdir_p(os.path.join(home_dir, ".cache")) - vistir.path.mkdir_p(os.path.join(home_dir, ".config")) - vistir.path.mkdir_p(os.path.join(home_dir, ".local", "share")) - vistir.path.mkdir_p(os.path.join(fake_root, "usr", "local", "share")) - vistir.path.mkdir_p(os.path.join(fake_root, "usr", "share")) + Path(os.path.join(home_dir, ".cache")).mkdir(exist_ok=True, parents=True) + Path(os.path.join(home_dir, ".config")).mkdir(exist_ok=True, parents=True) + Path(os.path.join(home_dir, ".local", "share")).mkdir(exist_ok=True, parents=True) + Path(os.path.join(fake_root, "usr", "local", "share")).mkdir(exist_ok=True, parents=True) + Path(os.path.join(fake_root, "usr", "share")).mkdir(exist_ok=True, parents=True) os.environ["XDG_DATA_DIRS"] = ":".join( [ os.path.join(fake_root, "usr", "local", "share"), @@ -150,15 +169,14 @@ def isolated_envdir(create_tmpdir): def setup_plugin(name): target = os.path.expandvars(os.path.expanduser("~/.{0}".format(name))) - this = vistir.compat.Path(__file__).absolute().parent + this = Path(__file__).absolute().parent plugin_dir = this / "test_artifacts" / name plugin_uri = plugin_dir.as_uri() if not "file:///" in plugin_uri and "file:/" in plugin_uri: plugin_uri = plugin_uri.replace("file:/", "file:///") - out, err = vistir.misc.run( - ["git", "clone", plugin_uri, vistir.compat.Path(target).as_posix()], nospin=True + out = subprocess.check_output( + ["git", "clone", plugin_uri, Path(target).as_posix()] ) - print(err, file=sys.stderr) print(out, file=sys.stderr) @@ -168,7 +186,7 @@ def build_python_versions(path, link_to=None): python_dir = path / python_name bin_dir = python_dir / "bin" bin_dir.mkdir(parents=True) - vistir.path.set_write_bit(bin_dir.as_posix()) + set_write_bit(bin_dir.as_posix()) executable_names = [ python_name, "python", @@ -239,7 +257,7 @@ def setup_pythons(isolated_envdir, monkeypatch): os.environ["PATH"] = os.environ.get("PATH").replace("::", ":") version_dicts = {"pyenv": pyenv_dict, "asdf": asdf_dict} shim_paths = [ - vistir.path.normalize_path(isolated_envdir.joinpath(p).as_posix()) + normalize_path(isolated_envdir.joinpath(p).as_posix()) for p in [".asdf/shims", ".pyenv/shims"] ] yield version_dicts @@ -255,7 +273,7 @@ def special_character_python(tmpdir): python_name = "2+" python_folder = tmpdir.mkdir(python_name) bin_dir = python_folder.mkdir("bin") - vistir.path.set_write_bit(bin_dir.strpath) + set_write_bit(bin_dir.strpath) python_path = bin_dir.join("python") os.link(python.path.as_posix(), python_path.strpath) return python_path @@ -263,7 +281,7 @@ def special_character_python(tmpdir): @pytest.fixture(autouse=True) def setup_env(): - with vistir.contextmanagers.temp_environ(): + with temp_environ(): os.environ["ANSI_COLORS_DISABLED"] = str("1") @@ -296,7 +314,7 @@ def _build_python_tuples(): name = str(version.version) else: name = version.name - path = vistir.path.normalize_path(v.path) + path = normalize_path(v.path) version = str(version.version) versions.append(pythoninfo(name, version, path, arch)) return versions @@ -308,21 +326,14 @@ def all_python_versions(): def get_windows_python_versions(): - c = vistir.misc.run( - "py -0p", - block=True, - nospin=True, - return_object=True, - combine_stderr=False, - write_to_stdout=False, - ) + out = subprocess.check_output("py -0p", shell=True) versions = [] for line in c.out.splitlines(): line = line.strip() if line and not "Installed Pythons found" in line: version, path = line.split("\t") version = version.strip().lstrip("-") - path = vistir.path.normalize_path(path.strip().strip('"')) + path = normalize_path(path.strip().strip('"')) arch = None if "-" in version: version, _, arch = version.partition("-") diff --git a/tests/test_python.py b/tests/test_python.py index 4c287ca..d436bc4 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -7,12 +7,21 @@ import pytest import six -import vistir from packaging.version import Version import pythonfinder -from .testutils import is_in_ospath, normalized_match, print_python_versions +from .testutils import ( + is_in_ospath, + normalize_path, + normalized_match, + print_python_versions, +) + +if sys.version_info[:2] < (3, 5): + from pathlib2 import Path +else: + from pathlib import Path def test_python_versions(monkeypatch, special_character_python): @@ -23,6 +32,9 @@ class FakeObj(object): def __init__(self, out): self.out = out + def communicate(self): + return self.out, "" + def kill(self): pass @@ -31,7 +43,7 @@ def kill(self): os.environ["PYTHONFINDER_IGNORE_UNSUPPORTED"] = str("1") with monkeypatch.context() as m: - m.setattr("vistir.misc.run", mock_version) + m.setattr("subprocess.Popen", mock_version) parsed = pythonfinder.models.python.PythonVersion.from_path( special_character_python.strpath ) @@ -70,6 +82,9 @@ class FakeObj(object): def __init__(self, out): self.out = out + def communicate(self): + return self.out, "" + def kill(self): pass @@ -83,7 +98,7 @@ def get_python_version(path, orig_fn=None): with monkeypatch.context() as m: os.environ["PYTHONFINDER_IGNORE_UNSUPPORTED"] = str("1") - m.setattr("vistir.misc.run", mock_version) + m.setattr("subprocess.Popen", mock_version) orig_run_fn = pythonfinder.utils.get_python_version get_pyversion = functools.partial(get_python_version, orig_fn=orig_run_fn) m.setattr("pythonfinder.utils.get_python_version", get_pyversion) @@ -96,7 +111,7 @@ def get_python_version(path, orig_fn=None): def test_shims_are_kept(monkeypatch, no_pyenv_root_envvar, setup_pythons, no_virtual_env): with monkeypatch.context() as m: os.environ["PATH"] = "{0}:{1}".format( - vistir.path.normalize_path("~/.pyenv/shims"), os.environ["PATH"] + normalize_path("~/.pyenv/shims"), os.environ["PATH"] ) f = pythonfinder.pythonfinder.Finder( global_search=True, system=False, ignore_unsupported=True @@ -110,14 +125,14 @@ def test_shims_are_kept(monkeypatch, no_pyenv_root_envvar, setup_pythons, no_vir # and used to trigger plugin setup -- this is true only if ``PYENV_ROOT`` is set` if shim_paths: assert ( - os.path.join(vistir.path.normalize_path("~/.pyenv/shims")) + os.path.join(normalize_path("~/.pyenv/shims")) not in f.system_path.path_order ), ( pythonfinder.environment.get_shim_paths() ) # "\n".join(f.system_path.path_order) else: assert ( - os.path.join(vistir.path.normalize_path("~/.pyenv/shims")) + os.path.join(normalize_path("~/.pyenv/shims")) in f.system_path.path_order ), "\n".join(f.system_path.path_order) python_versions = f.find_all_python_versions() diff --git a/tests/test_utils.py b/tests/test_utils.py index e7f0b39..2e7bcf5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,14 +1,19 @@ # -*- coding=utf-8 -*- import os +import sys from collections import namedtuple import pytest -import vistir import pythonfinder.utils from pythonfinder import Finder +if sys.version_info[:2] < (3, 5): + from pathlib2 import Path +else: + from pathlib import Path + os.environ["ANSI_COLORS_DISABLED"] = "1" pythoninfo = namedtuple("PythonVersion", ["version", "path", "arch"]) @@ -72,4 +77,4 @@ def test_parse_python_version(python, expected): def test_is_python(python, expected): assert pythonfinder.utils.path_is_known_executable(python) assert pythonfinder.utils.looks_like_python(os.path.basename(python)) - assert pythonfinder.utils.path_is_python(vistir.compat.Path(python)) + assert pythonfinder.utils.path_is_python(Path(python)) diff --git a/tests/testutils.py b/tests/testutils.py index c718475..aaa3013 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -1,15 +1,144 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, print_function +import atexit import itertools import os +import shutil +import stat +import sys +import tempfile +import warnings +from contextlib import contextmanager -import vistir +import click +import six + + +if sys.version_info[:2] < (3, 5): + from pathlib2 import Path +else: + from pathlib import Path + + +TRACKED_TEMPORARY_DIRECTORIES = [] + + +def set_write_bit(fn): + # type: (str) -> None + """ + Set read-write permissions for the current user on the target path. Fail silently + if the path doesn't exist. + + :param str fn: The target filename or path + :return: None + """ + + if not os.path.exists(fn): + return + file_stat = os.stat(fn).st_mode + os.chmod(fn, file_stat | stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + if not os.path.isdir(fn): + for path in [fn, os.path.dirname(fn)]: + try: + os.chflags(path, 0) + except AttributeError: + pass + return None + for root, dirs, files in os.walk(fn, topdown=False): + for dir_ in [os.path.join(root, d) for d in dirs]: + set_write_bit(dir_) + for file_ in [os.path.join(root, f) for f in files]: + set_write_bit(file_) + + +def create_tracked_tempdir(*args, **kwargs): + """Create a tracked temporary directory. + + This uses ``tempfile.mkdtemp``, but does not remove the directory when + the return value goes out of scope, instead registers a handler to cleanup + on program exit. + + The return value is the path to the created directory. + """ + + tempdir = tempfile.mkdtemp(*args, **kwargs) + TRACKED_TEMPORARY_DIRECTORIES.append(tempdir) + atexit.register(shutil.rmtree, tempdir) + if six.PY3: + warnings.simplefilter("ignore", ResourceWarning) + return tempdir + + +@contextmanager +def cd(path): + if not path: + return + prev_cwd = Path.cwd().as_posix() + if isinstance(path, Path): + path = path.as_posix() + os.chdir(str(path)) + try: + yield + finally: + os.chdir(prev_cwd) + + +@contextmanager +def temp_environ(): + """Allow the ability to set os.environ temporarily""" + environ = dict(os.environ) + try: + yield + finally: + os.environ.clear() + os.environ.update(environ) + + +def normalize_path(path): + # type: (AnyStr) -> AnyStr + """ + Return a case-normalized absolute variable-expanded path. + + :param str path: The non-normalized path + :return: A normalized, expanded, case-normalized path + :rtype: str + """ + + path = os.path.abspath(os.path.expandvars(os.path.expanduser(str(path)))) + if os.name == "nt" and os.path.exists(path): + try: + from ctypes import create_unicode_buffer, windll + except ImportError: + path = os.path.normpath(os.path.normcase(path)) + else: + BUFSIZE = 500 + buffer = create_unicode_buffer(BUFSIZE) + get_long_path_name = windll.kernel32.GetLongPathNameW + get_long_path_name(six.ensure_text(path), buffer, BUFSIZE) + path = buffer.value + return path + + return os.path.normpath(os.path.normcase(path)) + + +def is_in_path(path, parent): + # type: (AnyStr, AnyStr) -> bool + """ + Determine if the provided full path is in the given parent root. + + :param str path: The full path to check the location of. + :param str parent: The parent path to check for membership in + :return: Whether the full path is a member of the provided parent. + :rtype: bool + """ + + return normalize_path(str(path)).startswith(normalize_path(str(parent))) def normalized_match(path, parent): - return vistir.path.is_in_path( - vistir.path.normalize_path(path), vistir.path.normalize_path(parent) + return is_in_path( + normalize_path(path), normalize_path(parent) ) @@ -21,7 +150,7 @@ def is_in_ospath(path): def print_python_versions(versions): versions = list(versions) if versions: - vistir.misc.echo("Found python at the following locations:", err=True) + click.echo("Found python at the following locations:", err=True) for v in versions: py = v.py_version comes_from = getattr(py, "comes_from", None) @@ -29,7 +158,7 @@ def print_python_versions(versions): comes_from_path = getattr(comes_from, "path", v.path) else: comes_from_path = v.path - vistir.misc.echo( + click.echo( "{py.name!s}: {py.version!s} ({py.architecture!s}) @ {comes_from!s}".format( py=py, comes_from=comes_from_path ),