Skip to content

Commit 2048d15

Browse files
committed
Add support for just default requirements configs
Fixes #39 Signed-off-by: Pedro Algarvio <[email protected]>
1 parent 7f40b15 commit 2048d15

File tree

3 files changed

+124
-1
lines changed

3 files changed

+124
-1
lines changed

changelog/39.improvement.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for just default requirements configs

src/ptscripts/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77

88
import ptscripts.logs
99
from ptscripts.parser import Context
10+
from ptscripts.parser import DefaultToolsPythonRequirements
1011
from ptscripts.parser import DefaultVirtualEnv
1112
from ptscripts.parser import RegisteredImports
1213
from ptscripts.parser import command_group
1314

1415
if TYPE_CHECKING:
16+
from ptscripts.parser import DefaultRequirementsConfig
1517
from ptscripts.virtualenv import VirtualEnvConfig
1618

1719
__all__ = ["command_group", "register_tools_module", "Context"]
@@ -26,6 +28,13 @@ def register_tools_module(import_module: str, venv_config: VirtualEnvConfig | No
2628

2729
def set_default_virtualenv_config(venv_config: VirtualEnvConfig) -> None:
2830
"""
29-
Define the default tools requirements configuration.
31+
Define the default tools virtualenv configuration.
3032
"""
3133
DefaultVirtualEnv.set_default_virtualenv_config(venv_config)
34+
35+
36+
def set_default_requirements_config(reqs_config: DefaultRequirementsConfig) -> None:
37+
"""
38+
Define the default tools requirements configuration.
39+
"""
40+
DefaultToolsPythonRequirements.set_default_requirements_config(reqs_config)

src/ptscripts/parser.py

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

66
import argparse
7+
import hashlib
78
import importlib
89
import inspect
910
import logging
@@ -15,6 +16,7 @@
1516
from contextlib import AbstractContextManager
1617
from contextlib import contextmanager
1718
from contextlib import nullcontext
19+
from functools import cached_property
1820
from functools import partial
1921
from subprocess import CompletedProcess
2022
from types import FunctionType
@@ -26,6 +28,7 @@
2628
from typing import TypeVar
2729
from typing import cast
2830

31+
import attr
2932
import requests
3033
import rich
3134
from rich.console import Console
@@ -35,6 +38,7 @@
3538
from ptscripts import process
3639
from ptscripts.virtualenv import VirtualEnv
3740
from ptscripts.virtualenv import VirtualEnvConfig
41+
from ptscripts.virtualenv import _cast_to_pathlib_path
3842

3943
if sys.version_info < (3, 10):
4044
from typing_extensions import Concatenate
@@ -99,6 +103,82 @@ class FullArgumentOptions(ArgumentOptions):
99103
type: type[Any]
100104

101105

106+
@attr.s(frozen=True)
107+
class DefaultRequirementsConfig:
108+
"""
109+
Default tools requirements configuration typing.
110+
"""
111+
112+
requirements: list[str] = attr.ib(factory=list)
113+
requirements_files: list[pathlib.Path] = attr.ib(factory=list)
114+
pip_args: list[str] = attr.ib(factory=list)
115+
116+
@cached_property
117+
def requirements_hash(self) -> str:
118+
"""
119+
Returns a sha256 hash of the requirements.
120+
"""
121+
requirements_hash = hashlib.sha256()
122+
hash_seed = os.environ.get("TOOLS_VIRTUALENV_CACHE_SEED", "")
123+
requirements_hash.update(hash_seed.encode())
124+
if self.pip_args:
125+
for argument in self.pip_args:
126+
requirements_hash.update(argument.encode())
127+
if self.requirements:
128+
for requirement in sorted(self.requirements):
129+
requirements_hash.update(requirement.encode())
130+
if self.requirements_files:
131+
for fpath in sorted(self.requirements_files):
132+
with _cast_to_pathlib_path(fpath).open("rb") as rfh:
133+
try:
134+
digest = hashlib.file_digest(rfh, "sha256") # type: ignore[attr-defined]
135+
except AttributeError:
136+
# Python < 3.11
137+
buf = bytearray(2**18) # Reusable buffer to reduce allocations.
138+
view = memoryview(buf)
139+
digest = hashlib.sha256()
140+
while True:
141+
size = rfh.readinto(buf)
142+
if size == 0:
143+
break # EOF
144+
digest.update(view[:size])
145+
requirements_hash.update(digest.digest())
146+
return requirements_hash.hexdigest()
147+
148+
def install(self, ctx: Context) -> None:
149+
"""
150+
Install default requirements.
151+
"""
152+
from ptscripts.__main__ import TOOLS_VENVS_PATH
153+
154+
requirements_hash_file = TOOLS_VENVS_PATH / ".default-requirements.hash"
155+
if (
156+
requirements_hash_file.exists()
157+
and requirements_hash_file.read_text() == self.requirements_hash
158+
):
159+
# Requirements are up to date
160+
ctx.debug("Base tools requirements haven't changed.")
161+
return
162+
requirements = []
163+
if self.requirements_files:
164+
for fpath in self.requirements_files:
165+
requirements.extend(["-r", str(fpath)])
166+
if self.requirements:
167+
requirements.extend(self.requirements)
168+
if requirements:
169+
ctx.info("Installing base tools requirements ...")
170+
ctx.run(
171+
sys.executable,
172+
"-m",
173+
"pip",
174+
"install",
175+
*self.pip_args,
176+
*requirements,
177+
)
178+
requirements_hash_file.parent.mkdir(parents=True, exist_ok=True)
179+
requirements_hash_file.write_text(self.requirements_hash)
180+
181+
102182
class Context:
103183
"""
104184
Context class passed to every command group function as the first argument.
@@ -308,6 +388,35 @@ def set_default_virtualenv_config(cls, venv_config: VirtualEnvConfig) -> None:
308388
instance.venv_config = venv_config
309389

310390

391+
class DefaultToolsPythonRequirements:
392+
"""
393+
Simple class to hold registered imports.
394+
"""
395+
396+
_instance: DefaultToolsPythonRequirements | None = None
397+
reqs_config: DefaultRequirementsConfig | None
398+
399+
def __new__(cls) -> DefaultToolsPythonRequirements:
400+
"""
401+
Method that instantiates a singleton class and returns it.
402+
"""
403+
if cls._instance is None:
404+
instance = super().__new__(cls)
405+
instance.reqs_config = None
406+
cls._instance = instance
407+
return cls._instance
408+
409+
@classmethod
410+
def set_default_requirements_config(cls, reqs_config: DefaultRequirementsConfig) -> None:
411+
"""
412+
Set the default tools requirements configuration.
413+
"""
414+
instance = cls._instance
415+
if instance is None:
416+
instance = cls()
417+
instance.reqs_config = reqs_config
418+
419+
311420
class RegisteredImports:
312421
"""
313422
Simple class to hold registered imports.
@@ -444,6 +553,10 @@ def __new__(cls) -> Parser:
444553
return cls._instance
445554

446555
def _process_registered_tool_modules(self) -> None:
556+
default_reqs_config = DefaultToolsPythonRequirements().reqs_config
557+
if default_reqs_config:
558+
default_reqs_config.install(self.context)
559+
447560
default_venv: VirtualEnv | AbstractContextManager[None]
448561
default_venv_config = DefaultVirtualEnv().venv_config
449562
if default_venv_config:

0 commit comments

Comments
 (0)