diff --git a/poetry.lock b/poetry.lock index d7194f5a182..edbccd749f6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -384,15 +384,15 @@ testing = ["coverage", "nose"] [[package]] name = "platformdirs" -version = "2.5.1" +version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] [[package]] name = "pluggy" @@ -739,7 +739,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "2bf89b93e12d19fdadc3799785ef9cae5fd5d3d964ac2cfc4861b5e9d7e9554a" +content-hash = "daf3b6807272622969de0c6ca94eff7453bd904b9c17358687abe816d11a029a" [metadata.files] atomicwrites = [ @@ -1033,8 +1033,8 @@ pkginfo = [ {file = "pkginfo-1.8.2.tar.gz", hash = "sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff"}, ] platformdirs = [ - {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, - {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, diff --git a/pyproject.toml b/pyproject.toml index defd5ed760c..ccf3af34013 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,12 +43,13 @@ crashtest = "^0.3.0" entrypoints = "^0.3" html5lib = "^1.0" importlib-metadata = { version = ">=1.6.0", python = "<3.8" } -# packaging uses calver, so version is unclamped +# keyring uses calver, so version is unclamped keyring = ">=21.2.0" # packaging uses calver, so version is unclamped packaging = ">=20.4" pexpect = "^4.7.0" pkginfo = "^1.5" +platformdirs = "^2.5.2" requests = "^2.18" requests-toolbelt = "^0.9.1" shellingham = "^1.1" diff --git a/src/poetry/console/commands/config.py b/src/poetry/console/commands/config.py index 058e0560284..8559ae58d6c 100644 --- a/src/poetry/console/commands/config.py +++ b/src/poetry/console/commands/config.py @@ -58,7 +58,7 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any, Any]]: "cache-dir": ( str, lambda val: str(Path(val)), - str(Path(CACHE_DIR) / "virtualenvs"), + str(CACHE_DIR / "virtualenvs"), ), "virtualenvs.create": (boolean_validator, boolean_normalizer, True), "virtualenvs.in-project": (boolean_validator, boolean_normalizer, False), @@ -75,7 +75,7 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any, Any]]: "virtualenvs.path": ( str, lambda val: str(Path(val)), - str(Path(CACHE_DIR) / "virtualenvs"), + str(CACHE_DIR / "virtualenvs"), ), "virtualenvs.prefer-active-python": ( boolean_validator, @@ -112,7 +112,7 @@ def handle(self) -> int | None: from poetry.locations import CONFIG_DIR config = Factory.create_config(self.io) - config_file = TOMLFile(Path(CONFIG_DIR) / "config.toml") + config_file = TOMLFile(CONFIG_DIR / "config.toml") try: local_config_file = TOMLFile(self.poetry.file.parent / "poetry.toml") diff --git a/src/poetry/factory.py b/src/poetry/factory.py index 376baf00ed6..ef0ae390efb 100644 --- a/src/poetry/factory.py +++ b/src/poetry/factory.py @@ -2,7 +2,6 @@ import logging -from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import cast @@ -24,6 +23,8 @@ if TYPE_CHECKING: + from pathlib import Path + from cleo.io.io import IO from poetry.repositories.legacy_repository import LegacyRepository @@ -110,7 +111,7 @@ def create_config(cls, io: IO | None = None) -> Config: config = Config() # Load global config - config_file = TOMLFile(Path(CONFIG_DIR) / "config.toml") + config_file = TOMLFile(CONFIG_DIR / "config.toml") if config_file.exists(): if io.is_debug(): io.write_line( @@ -122,7 +123,7 @@ def create_config(cls, io: IO | None = None) -> Config: config.set_config_source(FileConfigSource(config_file)) # Load global auth config - auth_config_file = TOMLFile(Path(CONFIG_DIR) / "auth.toml") + auth_config_file = TOMLFile(CONFIG_DIR / "auth.toml") if auth_config_file.exists(): if io.is_debug(): io.write_line( diff --git a/src/poetry/locations.py b/src/poetry/locations.py index 5312619a02e..9ab526fd491 100644 --- a/src/poetry/locations.py +++ b/src/poetry/locations.py @@ -1,19 +1,42 @@ from __future__ import annotations +import logging import os +import sys from pathlib import Path -from poetry.utils.appdirs import user_cache_dir -from poetry.utils.appdirs import user_config_dir -from poetry.utils.appdirs import user_data_dir +from platformdirs import user_cache_path +from platformdirs import user_config_path +from platformdirs import user_data_path -CACHE_DIR = user_cache_dir("pypoetry") -DATA_DIR = user_data_dir("pypoetry") -CONFIG_DIR = user_config_dir("pypoetry") +logger = logging.getLogger(__name__) -REPOSITORY_CACHE_DIR = Path(CACHE_DIR) / "cache" / "repositories" +CACHE_DIR = user_cache_path("pypoetry", appauthor=False) +CONFIG_DIR = user_config_path("pypoetry", appauthor=False, roaming=True) + +REPOSITORY_CACHE_DIR = CACHE_DIR / "cache" / "repositories" + +# platformdirs 2.0.0 corrected the OSX/macOS config directory from +# /Users//Library/Application Support/ to +# /Users//Library/Preferences/. +# +# For now we only deprecate use of the old directory. +if sys.platform == "darwin": + _LEGACY_CONFIG_DIR = CONFIG_DIR.parent.parent / "Application Support" / "pypoetry" + config_toml = _LEGACY_CONFIG_DIR / "config.toml" + auth_toml = _LEGACY_CONFIG_DIR / "auth.toml" + + if any(file.exists() for file in (auth_toml, config_toml)): + logger.warn( + "Configuration file exists at %s, reusing this directory.\n\nConsider" + " moving configuration to %s, as support for the legacy directory will be" + " removed in an upcoming release.", + _LEGACY_CONFIG_DIR, + CONFIG_DIR, + ) + CONFIG_DIR = _LEGACY_CONFIG_DIR def data_dir() -> Path: @@ -21,4 +44,4 @@ def data_dir() -> Path: if poetry_home: return Path(poetry_home).expanduser() - return Path(user_data_dir("pypoetry", roaming=True)) + return user_data_path("pypoetry", appauthor=False, roaming=True) diff --git a/src/poetry/utils/appdirs.py b/src/poetry/utils/appdirs.py deleted file mode 100644 index 9b56e980400..00000000000 --- a/src/poetry/utils/appdirs.py +++ /dev/null @@ -1,224 +0,0 @@ -""" -This code was taken from https://github.com/ActiveState/appdirs and modified -to suit our purposes. -""" -from __future__ import annotations - -import os -import sys - - -def expanduser(path: str) -> str: - """ - Expand ~ and ~user constructions. - - Includes a workaround for http://bugs.python.org/issue14768 - """ - expanded = os.path.expanduser(path) - if path.startswith("~/") and expanded.startswith("//"): - expanded = expanded[1:] - return expanded - - -def user_cache_dir(appname: str) -> str: - r""" - Return full path to the user-specific cache dir for this application. - - "appname" is the name of application. - - Typical user cache directories are: - macOS: ~/Library/Caches/ - Unix: ~/.cache/ (XDG default) - Windows: C:\Users\\AppData\Local\\Cache - - On Windows the only suggestion in the MSDN docs is that local settings go - in the `CSIDL_LOCAL_APPDATA` directory. This is identical to the - non-roaming app data dir (the default returned by `user_data_dir`). Apps - typically put cache data somewhere *under* the given dir here. Some - examples: - ...\Mozilla\Firefox\Profiles\\Cache - ...\Acme\SuperApp\Cache\1.0 - - OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value. - """ - if sys.platform == "win32": - # Get the base path - path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA")) - - # Add our app name and Cache directory to it - path = os.path.join(path, appname, "Cache") - elif sys.platform == "darwin": - # Get the base path - path = expanduser("~/Library/Caches") - - # Add our app name to it - path = os.path.join(path, appname) - else: - # Get the base path - path = os.getenv("XDG_CACHE_HOME", expanduser("~/.cache")) - - # Add our app name to it - path = os.path.join(path, appname) - - return path - - -def user_data_dir(appname: str, roaming: bool = False) -> str: - r""" - Return full path to the user-specific data dir for this application. - - "appname" is the name of application. - If None, just the system directory is returned. - "roaming" (boolean, default False) can be set True to use the Windows - roaming appdata directory. That means that for users on a Windows - network setup for roaming profiles, this user data will be - sync'd on login. See - - for a discussion of issues. - - Typical user data directories are: - macOS: ~/Library/Application Support/ - Unix: ~/.local/share/ # or in - $XDG_DATA_HOME, if defined - Win XP (not roaming): C:\Documents and Settings\\ ... - ...Application Data\ - Win XP (roaming): C:\Documents and Settings\\Local ... - ...Settings\Application Data\ - Win 7 (not roaming): C:\Users\\AppData\Local\ - Win 7 (roaming): C:\Users\\AppData\Roaming\ - - For Unix, we follow the XDG spec and support $XDG_DATA_HOME. - That means, by default "~/.local/share/". - """ - if sys.platform == "win32": - const = "CSIDL_APPDATA" if roaming else "CSIDL_LOCAL_APPDATA" - return os.path.join(os.path.normpath(_get_win_folder(const)), appname) - elif sys.platform == "darwin": - return os.path.join(expanduser("~/Library/Application Support/"), appname) - else: - return os.path.join( - os.getenv("XDG_DATA_HOME", expanduser("~/.local/share")), appname - ) - - -def user_config_dir(appname: str, roaming: bool = True) -> str: - """Return full path to the user-specific config dir for this application. - - "appname" is the name of application. - If None, just the system directory is returned. - "roaming" (boolean, default True) can be set False to not use the - Windows roaming appdata directory. That means that for users on a - Windows network setup for roaming profiles, this user data will be - sync'd on login. See - - for a discussion of issues. - - Typical user data directories are: - macOS: same as user_data_dir - Unix: ~/.config/ - Win *: same as user_data_dir - - For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME. - That means, by default "~/.config/". - """ - if sys.platform == "win32": - path = user_data_dir(appname, roaming=roaming) - elif sys.platform == "darwin": - path = user_data_dir(appname) - else: - path = os.getenv("XDG_CONFIG_HOME", expanduser("~/.config")) - path = os.path.join(path, appname) - - return path - - -# for the discussion regarding site_config_dirs locations -# see -def site_config_dirs(appname: str) -> list[str]: - r"""Return a list of potential user-shared config dirs for this application. - - "appname" is the name of application. - - Typical user config directories are: - macOS: /Library/Application Support// - Unix: /etc or $XDG_CONFIG_DIRS[i]// for each value in - $XDG_CONFIG_DIRS - Win XP: C:\Documents and Settings\All Users\Application ... - ...Data\\ - Vista: (Fail! "C:\ProgramData" is a hidden *system* directory - on Vista.) - Win 7: Hidden, but writeable on Win 7: - C:\ProgramData\\ - """ - if sys.platform == "win32": - path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA")) - pathlist = [os.path.join(path, appname)] - elif sys.platform == "darwin": - pathlist = [os.path.join("/Library/Application Support", appname)] - else: - # try looking in $XDG_CONFIG_DIRS - xdg_config_dirs = os.getenv("XDG_CONFIG_DIRS", "/etc/xdg") - if xdg_config_dirs: - pathlist = [ - os.path.join(expanduser(x), appname) - for x in xdg_config_dirs.split(os.pathsep) - ] - else: - pathlist = [] - - # always look in /etc directly as well - pathlist.append("/etc") - - return pathlist - - -if sys.platform == "win32": - - def _get_win_folder_from_registry(csidl_name: str) -> str: - """ - This is a fallback technique at best. I'm not sure if using the - registry for this guarantees us the correct answer for all CSIDL_* - names. - """ - import _winreg - - shell_folder_name = { - "CSIDL_APPDATA": "AppData", - "CSIDL_COMMON_APPDATA": "Common AppData", - "CSIDL_LOCAL_APPDATA": "Local AppData", - }[csidl_name] - - key = _winreg.OpenKey( - _winreg.HKEY_CURRENT_USER, - r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders", - ) - directory, _type = _winreg.QueryValueEx(key, shell_folder_name) - return directory - - def _get_win_folder_with_ctypes(csidl_name: str) -> str: - csidl_const = { - "CSIDL_APPDATA": 26, - "CSIDL_COMMON_APPDATA": 35, - "CSIDL_LOCAL_APPDATA": 28, - }[csidl_name] - - buf = ctypes.create_unicode_buffer(1024) - windll = ctypes.windll - windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) - - # Downgrade to short path name if have highbit chars. See - # . - has_high_char = any(ord(c) > 255 for c in buf) - if has_high_char: - buf2 = ctypes.create_unicode_buffer(1024) - if windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): - buf = buf2 - - return buf.value - - try: - import ctypes - - _get_win_folder = _get_win_folder_with_ctypes - except ImportError: - _get_win_folder = _get_win_folder_from_registry diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py index 62ae9a0dde0..81def97c285 100644 --- a/src/poetry/utils/env.py +++ b/src/poetry/utils/env.py @@ -534,7 +534,7 @@ def _detect_active_python(self, io: IO) -> str: def activate(self, python: str, io: IO) -> Env: venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: - venv_path = Path(CACHE_DIR) / "virtualenvs" + venv_path = CACHE_DIR / "virtualenvs" else: venv_path = Path(venv_path) @@ -627,7 +627,7 @@ def activate(self, python: str, io: IO) -> Env: def deactivate(self, io: IO) -> None: venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: - venv_path = Path(CACHE_DIR) / "virtualenvs" + venv_path = CACHE_DIR / "virtualenvs" else: venv_path = Path(venv_path) @@ -653,7 +653,7 @@ def get(self, reload: bool = False) -> VirtualEnv | SystemEnv: venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: - venv_path = Path(CACHE_DIR) / "virtualenvs" + venv_path = CACHE_DIR / "virtualenvs" else: venv_path = Path(venv_path) @@ -694,7 +694,7 @@ def get(self, reload: bool = False) -> VirtualEnv | SystemEnv: venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: - venv_path = Path(CACHE_DIR) / "virtualenvs" + venv_path = CACHE_DIR / "virtualenvs" else: venv_path = Path(venv_path) @@ -724,7 +724,7 @@ def list(self, name: str | None = None) -> list[VirtualEnv]: venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: - venv_path = Path(CACHE_DIR) / "virtualenvs" + venv_path = CACHE_DIR / "virtualenvs" else: venv_path = Path(venv_path) @@ -744,7 +744,7 @@ def list(self, name: str | None = None) -> list[VirtualEnv]: def remove(self, python: str) -> Env: venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: - venv_path = Path(CACHE_DIR) / "virtualenvs" + venv_path = CACHE_DIR / "virtualenvs" else: venv_path = Path(venv_path) @@ -866,7 +866,7 @@ def create_venv( if root_venv: venv_path = cwd / ".venv" elif venv_path is None: - venv_path = Path(CACHE_DIR) / "virtualenvs" + venv_path = CACHE_DIR / "virtualenvs" else: venv_path = Path(venv_path) diff --git a/tests/conftest.py b/tests/conftest.py index 7d7bf37f485..af9131ace5c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -212,12 +212,12 @@ def config( @pytest.fixture() -def config_dir(tmp_dir: str) -> str: - return tempfile.mkdtemp(prefix="poetry_config_", dir=tmp_dir) +def config_dir(tmp_dir: str) -> Path: + return Path(tempfile.mkdtemp(prefix="poetry_config_", dir=tmp_dir)) @pytest.fixture(autouse=True) -def mock_user_config_dir(mocker: MockerFixture, config_dir: str) -> None: +def mock_user_config_dir(mocker: MockerFixture, config_dir: Path) -> None: mocker.patch("poetry.locations.CONFIG_DIR", new=config_dir) mocker.patch("poetry.factory.CONFIG_DIR", new=config_dir)