Files
2025-02-25 18:35:54 +08:00

275 lines
8.2 KiB
Python

"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from pathlib import Path
from typing import Optional
from importlib import import_module, invalidate_caches
import pkg_resources
from subprocess import run
import shutil
from plugin_utils.utils import get_qgis_python_path
_user_data_env_var = "SPECKLE_USERDATA_PATH"
_debug = False
_vs_code_directory = os.path.expanduser(
"~\.vscode\extensions\ms-python.python-2023.20.0\pythonFiles\lib\python"
)
def _path() -> Optional[Path]:
"""Read the user data path override setting."""
path_override = os.environ.get(_user_data_env_var)
if path_override:
return Path(path_override)
return None
_application_name = "Speckle"
def override_application_name(application_name: str) -> None:
"""Override the global Speckle application name."""
global _application_name
_application_name = application_name
def override_application_data_path(path: Optional[str]) -> None:
"""
Override the global Speckle application data path.
If the value of path is `None` the environment variable gets deleted.
"""
if path:
os.environ[_user_data_env_var] = path
else:
os.environ.pop(_user_data_env_var, None)
def _ensure_folder_exists(base_path: Path, folder_name: str) -> Path:
path = base_path.joinpath(folder_name)
path.mkdir(exist_ok=True, parents=True)
return path
def user_application_data_path() -> Path:
"""Get the platform specific user configuration folder path"""
path_override = _path()
if path_override:
return path_override
try:
if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA")
if not app_data_path:
raise Exception("Cannot get appdata path from environment.")
return Path(app_data_path)
else:
# try getting the standard XDG_DATA_HOME value
# as that is used as an override
app_data_path = os.getenv("XDG_DATA_HOME")
if app_data_path:
return Path(app_data_path)
else:
return _ensure_folder_exists(Path.home(), ".config")
except Exception as ex:
raise Exception("Failed to initialize user application data path.", ex)
def user_speckle_folder_path() -> Path:
"""Get the folder where the user's Speckle data should be stored."""
return _ensure_folder_exists(user_application_data_path(), _application_name)
def user_speckle_connector_installation_path(host_application: str) -> Path:
"""
Gets a connector specific installation folder.
In this folder we can put our connector installation and all python packages.
"""
return _ensure_folder_exists(
_ensure_folder_exists(user_speckle_folder_path(), "connector_installations"),
host_application,
)
print("Starting module dependency installation")
print(sys.executable)
PYTHON_PATH = get_qgis_python_path()
def connector_installation_path(host_application: str) -> Path:
connector_installation_path = user_speckle_connector_installation_path(
host_application
)
connector_installation_path.mkdir(exist_ok=True, parents=True)
# set user modules path at beginning of paths for earlier hit
if sys.path[0] != connector_installation_path:
sys.path.insert(0, str(connector_installation_path))
print(f"Using connector installation path {connector_installation_path}")
return connector_installation_path
def is_pip_available() -> bool:
try:
import_module("pip") # noqa F401
return True
except ImportError:
return False
def ensure_pip() -> None:
print("Installing pip... ")
print(PYTHON_PATH)
completed_process = run([PYTHON_PATH, "-m", "ensurepip"])
if completed_process.returncode == 0:
print("Successfully installed pip")
else:
raise Exception(
f"Failed to install pip, got {completed_process.returncode} return code"
)
def get_requirements_path() -> Path:
# we assume that a requirements.txt exists next to the __init__.py file
if sys.platform.lower().startswith("darwin"):
path = Path(Path(__file__).parent, "requirements_mac.txt")
path = Path(Path(__file__).parent, "requirements.txt")
assert path.exists(), f"path not found {path}"
return path
def _dependencies_installed(requirements: str, path: str) -> bool:
for d in pkg_resources.find_distributions(path):
entry = f"{d.key}=={d.version}"
if entry in requirements:
requirements = requirements.replace(entry, "")
requirements = requirements.replace(" ", "").replace(";", "").replace(",", "")
if len(requirements) > 0:
return False
print("Dependencies already installed")
return True
def install_requirements(host_application: str) -> None:
# set up addons/modules under the user
# script path. Here we'll install the
# dependencies
requirements = get_requirements_path().read_text().replace("\n", "")
path = str(connector_installation_path(host_application))
print(f"Installing debugpy to {path}")
if _dependencies_installed(requirements, path):
return
try:
shutil.rmtree(path)
except PermissionError as e:
raise Exception("Restart QGIS for changes to take effect")
print(f"Installing Speckle dependencies to {path}")
from subprocess import run
completed_process = run(
[
PYTHON_PATH,
"-m",
"pip",
"install",
"--pre",
"-t",
str(path),
"-r",
str(get_requirements_path()),
],
capture_output=True,
)
if completed_process.returncode != 0:
m = f"Failed to install dependenices through pip, got {completed_process.returncode} as return code. Full log: {completed_process}"
print(m)
print(completed_process.stdout)
print(completed_process.stderr)
raise Exception(m)
def install_dependencies(host_application: str) -> None:
if not is_pip_available():
ensure_pip()
install_requirements(host_application)
def _import_dependencies() -> None:
import_module("specklepy")
# the code above doesn't work for now, it fails on importing graphql-core
# despite that, the connector seams to be working as expected
# But it would be nice to make this solution work
# it would ensure that all dependencies are fully loaded
# requirements = get_requirements_path().read_text()
# reqs = [
# req.split(" ; ")[0].split("==")[0].split("[")[0].replace("-", "_")
# for req in requirements.split("\n")
# if req and not req.startswith(" ")
# ]
# for req in reqs:
# print(req)
# import_module("specklepy")
def ensure_dependencies(host_application: str) -> None:
try:
install_dependencies(host_application)
invalidate_caches()
# _import_dependencies()
print("Successfully found dependencies")
except ImportError:
raise Exception(
f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!"
)
def startDebugger() -> None:
if _debug is True:
try:
import debugpy
except:
# path = str(connector_installation_path(host_application))
completed_process = run(
[
PYTHON_PATH,
"-m",
"pip",
"install",
"debugpy==1.8.0",
],
capture_output=True,
)
if completed_process.returncode != 0:
m = f"Failed to install debugpy through pip. Disable debug mode or install debugpy manually. Full log: {completed_process}"
raise Exception(completed_process)
# debugger: https://gist.github.com/giohappy/8a30f14678aa7e446f9b694c632d7089
if _debug is True:
import debugpy
sys.path.append(_vs_code_directory)
debugpy.configure(python=PYTHON_PATH) # shutil.which("python"))
debugpy.listen(("localhost", 5678))
debugpy.wait_for_client()
# path = str(connector_installation_path("QGIS"))
# print(path)