Skip to content

Commit 72dd00c

Browse files
committed
Switch to using a default virtualenv
Signed-off-by: Pedro Algarvio <[email protected]>
1 parent f751925 commit 72dd00c

File tree

4 files changed

+59
-148
lines changed

4 files changed

+59
-148
lines changed

src/ptscripts/__init__.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,16 @@
55
import sys
66
from typing import TYPE_CHECKING
77

8-
CWD: pathlib.Path = pathlib.Path.cwd()
9-
108
import ptscripts.logs
119
from ptscripts.parser import Context
12-
from ptscripts.parser import DefaultToolsPythonRequirements
10+
from ptscripts.parser import DefaultVirtualEnv
1311
from ptscripts.parser import RegisteredImports
1412
from ptscripts.parser import command_group
1513

1614
if TYPE_CHECKING:
17-
from ptscripts.parser import DefaultRequirementsConfig
1815
from ptscripts.virtualenv import VirtualEnvConfig
1916

20-
__all__ = ["command_group", "register_tools_module", "Context", "CWD"]
17+
__all__ = ["command_group", "register_tools_module", "Context"]
2118

2219

2320
def register_tools_module(import_module: str, venv_config: VirtualEnvConfig | None = None) -> None:
@@ -27,8 +24,8 @@ def register_tools_module(import_module: str, venv_config: VirtualEnvConfig | No
2724
RegisteredImports.register_import(import_module, venv_config=venv_config)
2825

2926

30-
def set_default_requirements_config(reqs_config: DefaultRequirementsConfig) -> None:
27+
def set_default_virtualenv_config(venv_config: VirtualEnvConfig) -> None:
3128
"""
3229
Define the default tools requirements configuration.
3330
"""
34-
DefaultToolsPythonRequirements.set_default_requirements_config(reqs_config)
31+
DefaultVirtualEnv.set_default_virtualenv_config(venv_config)

src/ptscripts/__main__.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
22

33
import logging
44
import os
5+
import pathlib
56
import sys
67
from typing import NoReturn
78

8-
from ptscripts.parser import TOOLS_DEPS_PATH
99
from ptscripts.parser import Parser
1010

11-
if str(TOOLS_DEPS_PATH) in sys.path and sys.path[0] != str(TOOLS_DEPS_PATH):
12-
sys.path.remove(str(TOOLS_DEPS_PATH))
13-
if TOOLS_DEPS_PATH not in sys.path:
14-
sys.path.insert(0, str(TOOLS_DEPS_PATH))
11+
CWD: pathlib.Path = pathlib.Path.cwd()
12+
if "TOOLS_SCRIPTS_PATH" in os.environ:
13+
_BASE_PATH = pathlib.Path(os.environ["TOOLS_SCRIPTS_PATH"]).expanduser()
14+
else:
15+
_BASE_PATH = CWD
16+
TOOLS_VENVS_PATH = _BASE_PATH / ".tools-venvs"
17+
18+
DEFAULT_TOOLS_VENV_PATH = TOOLS_VENVS_PATH / "default"
19+
if str(DEFAULT_TOOLS_VENV_PATH) in sys.path:
20+
sys.path.remove(str(DEFAULT_TOOLS_VENV_PATH))
1521

1622
log = logging.getLogger(__name__)
1723

src/ptscripts/parser.py

Lines changed: 34 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from __future__ import annotations
55

66
import argparse
7-
import hashlib
87
import importlib
98
import inspect
109
import logging
@@ -16,7 +15,6 @@
1615
from contextlib import AbstractContextManager
1716
from contextlib import contextmanager
1817
from contextlib import nullcontext
19-
from functools import cached_property
2018
from functools import partial
2119
from subprocess import CompletedProcess
2220
from types import FunctionType
@@ -28,18 +26,15 @@
2826
from typing import TypeVar
2927
from typing import cast
3028

31-
import attr
3229
import requests
3330
import rich
3431
from rich.console import Console
3532
from rich.theme import Theme
3633

37-
from ptscripts import CWD
3834
from ptscripts import logs
3935
from ptscripts import process
4036
from ptscripts.virtualenv import VirtualEnv
4137
from ptscripts.virtualenv import VirtualEnvConfig
42-
from ptscripts.virtualenv import _cast_to_pathlib_path
4338

4439
if sys.version_info < (3, 10):
4540
from typing_extensions import Concatenate
@@ -76,15 +71,6 @@
7671
OriginalFunc = Callable[Param, RetType]
7772
DecoratedFunc = Callable[Concatenate[str, Param], RetType]
7873

79-
if "TOOLS_SCRIPTS_PATH" in os.environ:
80-
TOOLS_SCRIPTS_PATH = pathlib.Path(os.environ["TOOLS_SCRIPTS_PATH"]).resolve()
81-
else:
82-
TOOLS_SCRIPTS_PATH = CWD
83-
84-
TOOLS_BASE_PATH = TOOLS_SCRIPTS_PATH / ".tools"
85-
TOOLS_DEPS_PATH = TOOLS_BASE_PATH / "deps"
86-
87-
8874
log = logging.getLogger(__name__)
8975

9076

@@ -293,122 +279,33 @@ def web(self) -> requests.Session:
293279
return requests.Session()
294280

295281

296-
@attr.s(frozen=True)
297-
class DefaultRequirementsConfig:
298-
"""
299-
Default tools requirements configuration typing.
300-
"""
301-
302-
requirements: list[str] = attr.ib(factory=list)
303-
requirements_files: list[pathlib.Path] = attr.ib(factory=list)
304-
pip_args: list[str] = attr.ib(factory=list)
305-
base_tools_dir: pathlib.Path = attr.ib(init=False)
306-
deps_dir: pathlib.Path = attr.ib(init=False)
307-
308-
@base_tools_dir.default
309-
def _default_base_tools_dir(self) -> pathlib.Path:
310-
base_tools_path = TOOLS_SCRIPTS_PATH / ".tools"
311-
base_tools_path.mkdir(exist_ok=True)
312-
return base_tools_path
313-
314-
@deps_dir.default
315-
def _default_deps_dir(self) -> pathlib.Path:
316-
default_deps_dir = self.base_tools_dir / "deps"
317-
default_deps_dir.mkdir(exist_ok=True)
318-
return default_deps_dir
319-
320-
@cached_property
321-
def requirements_hash(self) -> str:
322-
"""
323-
Returns a sha256 hash of the requirements.
324-
"""
325-
requirements_hash = hashlib.sha256()
326-
hash_seed = os.environ.get("TOOLS_VIRTUALENV_CACHE_SEED", "")
327-
requirements_hash.update(hash_seed.encode())
328-
if self.pip_args:
329-
for argument in self.pip_args:
330-
requirements_hash.update(argument.encode())
331-
if self.requirements:
332-
for requirement in sorted(self.requirements):
333-
requirements_hash.update(requirement.encode())
334-
if self.requirements_files:
335-
for fpath in sorted(self.requirements_files):
336-
with _cast_to_pathlib_path(fpath).open("rb") as rfh:
337-
try:
338-
digest = hashlib.file_digest(rfh, "sha256") # type: ignore[attr-defined]
339-
except AttributeError:
340-
# Python < 3.11
341-
buf = bytearray(2**18) # Reusable buffer to reduce allocations.
342-
view = memoryview(buf)
343-
digest = hashlib.sha256()
344-
while True:
345-
size = rfh.readinto(buf)
346-
if size == 0:
347-
break # EOF
348-
digest.update(view[:size])
349-
requirements_hash.update(digest.digest())
350-
return requirements_hash.hexdigest()
351-
352-
def install(self, ctx: Context) -> None:
353-
"""
354-
Install default requirements.
355-
"""
356-
requirements_hash_file = self.deps_dir / ".requirements.hash"
357-
if (
358-
requirements_hash_file.exists()
359-
and requirements_hash_file.read_text() == self.requirements_hash
360-
):
361-
# Requirements are up to date
362-
ctx.debug("Base tools requirements haven't changed.")
363-
return
364-
requirements = []
365-
if self.requirements_files:
366-
for fpath in self.requirements_files:
367-
requirements.extend(["-r", str(fpath)])
368-
if self.requirements:
369-
requirements.extend(self.requirements)
370-
if requirements:
371-
ctx.info("Installing base tools requirements ...")
372-
ctx.run(
373-
sys.executable,
374-
"-m",
375-
"pip",
376-
"install",
377-
"--prefix",
378-
str(self.deps_dir),
379-
*self.pip_args,
380-
*requirements,
381-
)
382-
requirements_hash_file.write_text(self.requirements_hash)
383-
384-
385-
class DefaultToolsPythonRequirements:
282+
class DefaultVirtualEnv:
386283
"""
387284
Simple class to hold registered imports.
388285
"""
389286

390-
_instance: DefaultToolsPythonRequirements | None = None
391-
reqs_config: DefaultRequirementsConfig | None
287+
_instance: DefaultVirtualEnv | None = None
288+
venv_config: VirtualEnvConfig | None
392289

393-
def __new__(cls) -> DefaultToolsPythonRequirements:
290+
def __new__(cls) -> DefaultVirtualEnv:
394291
"""
395292
Method that instantiates a singleton class and returns it.
396293
"""
397294
if cls._instance is None:
398295
instance = super().__new__(cls)
399-
instance.reqs_config = None
296+
instance.venv_config = None
400297
cls._instance = instance
401298
return cls._instance
402299

403300
@classmethod
404-
def set_default_requirements_config(cls, reqs_config: DefaultRequirementsConfig) -> None:
301+
def set_default_virtualenv_config(cls, venv_config: VirtualEnvConfig) -> None:
405302
"""
406303
Set the default tools requirements configuration.
407304
"""
408305
instance = cls._instance
409306
if instance is None:
410307
instance = cls()
411-
instance.reqs_config = reqs_config
308+
instance.venv_config = venv_config
412309

413310

414311
class RegisteredImports:
@@ -547,25 +444,33 @@ def __new__(cls) -> Parser:
547444
return cls._instance
548445

549446
def _process_registered_tool_modules(self) -> None:
550-
default_reqs_config = DefaultToolsPythonRequirements().reqs_config
551-
if default_reqs_config:
552-
default_reqs_config.install(self.context)
553-
for module_name, venv_config in RegisteredImports():
554-
venv: VirtualEnv | AbstractContextManager[None]
555-
if venv_config:
556-
if "name" not in venv_config:
557-
venv_config["name"] = module_name
558-
venv = VirtualEnv(ctx=self.context, **venv_config)
559-
else:
560-
venv = nullcontext()
561-
with venv:
562-
try:
563-
importlib.import_module(module_name)
564-
except ImportError as exc:
565-
if os.environ.get("TOOLS_IGNORE_IMPORT_ERRORS", "0") == "0":
566-
self.context.warn(
567-
f"Could not import the registered tools module {module_name!r}: {exc}"
568-
)
447+
default_venv: VirtualEnv | AbstractContextManager[None]
448+
default_venv_config = DefaultVirtualEnv().venv_config
449+
if default_venv_config:
450+
if "name" not in default_venv_config:
451+
default_venv_config["name"] = "default"
452+
default_venv_config["system_site_packages"] = True
453+
default_venv_config["add_as_extra_site_packages"] = True
454+
default_venv = VirtualEnv(ctx=self.context, **default_venv_config)
455+
else:
456+
default_venv = nullcontext()
457+
with default_venv:
458+
for module_name, venv_config in RegisteredImports():
459+
venv: VirtualEnv | AbstractContextManager[None]
460+
if venv_config:
461+
if "name" not in venv_config:
462+
venv_config["name"] = module_name
463+
venv = VirtualEnv(ctx=self.context, **venv_config)
464+
else:
465+
venv = nullcontext()
466+
with venv:
467+
try:
468+
importlib.import_module(module_name)
469+
except ImportError as exc:
470+
if os.environ.get("TOOLS_IGNORE_IMPORT_ERRORS", "0") == "0":
471+
self.context.warn(
472+
f"Could not import the registered tools module {module_name!r}: {exc}"
473+
)
569474

570475
def parse_args(self) -> None:
571476
"""

src/ptscripts/virtualenv.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121

2222
import attr
2323

24-
from ptscripts import CWD
25-
2624
if TYPE_CHECKING:
2725
from ptscripts.parser import Context
2826

@@ -84,11 +82,10 @@ def _default_setuptools_requirement(self) -> str:
8482

8583
@venv_dir.default
8684
def _default_venv_dir(self) -> pathlib.Path:
87-
if "TOOLS_SCRIPTS_PATH" in os.environ:
88-
base_path = pathlib.Path(os.environ["TOOLS_SCRIPTS_PATH"])
89-
else:
90-
base_path = CWD
91-
venvs_path = base_path / ".tools" / "venvs"
85+
# Late import to avoid circular import errors
86+
from ptscripts.__main__ import TOOLS_VENVS_PATH
87+
88+
venvs_path = TOOLS_VENVS_PATH
9289
venvs_path.mkdir(parents=True, exist_ok=True)
9390
return venvs_path / self.name
9491

@@ -158,6 +155,9 @@ def _install_requirements(self) -> None:
158155
self.venv_dir.joinpath(".requirements.hash").write_text(self.requirements_hash)
159156

160157
def _create_virtualenv(self) -> None:
158+
# Late import to avoid circular import errors
159+
from ptscripts.__main__ import CWD
160+
161161
if self.venv_dir.exists():
162162
self.ctx.debug("Virtual environment path already exists")
163163
return
@@ -254,6 +254,9 @@ def run(self, *args: str, **kwargs) -> CompletedProcess[bytes]:
254254
"""
255255
Run a command in the context of the virtual environment.
256256
"""
257+
# Late import to avoid circular import errors
258+
from ptscripts.__main__ import CWD
259+
257260
kwargs.setdefault("cwd", CWD)
258261
env = kwargs.pop("env", None)
259262
environ = self.environ.copy()

0 commit comments

Comments
 (0)