Skip to content

Commit 5e4864d

Browse files
committed
Added several virtualenv improvements.
Fixes #25 Fixes #26 Fixes #27 Signed-off-by: Pedro Algarvio <[email protected]>
1 parent cc61fdd commit 5e4864d

File tree

6 files changed

+119
-29
lines changed

6 files changed

+119
-29
lines changed

changelog/25.improvement.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Allow importing the virtualenv's dependencies into the python running ``tools``.
2+
This will allow maintaining a lighter ``tools.txt`` requirements file, and install additional dependencies only for the commands that really need them.

changelog/26.improvement.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for a default Virtualenv. The default virtualenv site-packages will be added to the running python as an extra site dir.

changelog/27.improvement.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow passing virtualenv configuration when calling ``ptscripts.register_tools_module``

src/ptscripts/__init__.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,24 @@
66

77
import ptscripts.logs
88
from ptscripts.parser import command_group
9-
from ptscripts.parser import Context, RegisteredImports
9+
from ptscripts.parser import Context, RegisteredImports, DefaultVirtualenvConfig
10+
from ptscripts.virtualenv import VirtualEnvConfig
1011

1112
__all__ = ["command_group", "register_tools_module", "Context", "CWD"]
1213

1314

14-
def register_tools_module(import_module: str) -> None:
15+
def register_tools_module(import_module: str, venv_config: VirtualEnvConfig | None = None) -> None:
1516
"""
1617
Register a module to be imported when instantiating the tools parser.
1718
"""
18-
RegisteredImports.register_import(import_module)
19+
RegisteredImports.register_import(import_module, venv_config=venv_config)
20+
21+
22+
def set_default_venv_config(venv_config: VirtualEnvConfig) -> None:
23+
"""
24+
Define the default virtualenv configuration.
25+
26+
This virtualenv will be available to all commands, and it's ``site-packages``
27+
dir(s) will be added to the current python interpreter site.
28+
"""
29+
DefaultVirtualenvConfig.set_default_venv_config(venv_config)

src/ptscripts/parser.py

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
import typing
1515
from collections.abc import Iterator
1616
from contextlib import contextmanager
17+
from contextlib import nullcontext
1718
from functools import partial
1819
from subprocess import CompletedProcess
1920
from types import FunctionType
2021
from types import GenericAlias
2122
from typing import Any
2223
from typing import cast
24+
from typing import ContextManager
2325
from typing import TYPE_CHECKING
2426
from typing import TypedDict
2527

@@ -248,38 +250,70 @@ def web(self) -> requests.Session:
248250
return requests.Session()
249251

250252

253+
class DefaultVirtualenvConfig:
254+
"""
255+
Simple class to hold registered imports.
256+
"""
257+
258+
_instance: DefaultVirtualenvConfig | None = None
259+
venv_config: VirtualEnvConfig
260+
261+
def __new__(cls):
262+
"""
263+
Method that instantiates a singleton class and returns it.
264+
"""
265+
if cls._instance is None:
266+
instance = super().__new__(cls)
267+
cls._instance = instance
268+
return cls._instance
269+
270+
@classmethod
271+
def set_default_venv_config(cls, venv_config: VirtualEnvConfig) -> None:
272+
"""
273+
Register an import.
274+
"""
275+
instance = cls._instance
276+
if instance is None:
277+
instance = cls()
278+
if venv_config and "name" not in venv_config:
279+
venv_config["name"] = "default"
280+
instance.venv_config = venv_config
281+
282+
251283
class RegisteredImports:
252284
"""
253285
Simple class to hold registered imports.
254286
"""
255287

256288
_instance: RegisteredImports | None = None
257-
_registered_imports: list[str]
289+
_registered_imports: dict[str, VirtualEnvConfig | None]
258290

259291
def __new__(cls):
260292
"""
261293
Method that instantiates a singleton class and returns it.
262294
"""
263295
if cls._instance is None:
264296
instance = super().__new__(cls)
265-
instance._registered_imports = []
297+
instance._registered_imports = {}
266298
cls._instance = instance
267299
return cls._instance
268300

269301
@classmethod
270-
def register_import(cls, import_module: str) -> None:
302+
def register_import(
303+
cls, import_module: str, venv_config: VirtualEnvConfig | None = None
304+
) -> None:
271305
"""
272306
Register an import.
273307
"""
274308
instance = cls()
275309
if import_module not in instance._registered_imports:
276-
instance._registered_imports.append(import_module)
310+
instance._registered_imports[import_module] = venv_config
277311

278312
def __iter__(self):
279313
"""
280314
Return an iterator of all registered imports.
281315
"""
282-
return iter(self._registered_imports)
316+
return iter(self._registered_imports.items())
283317

284318

285319
class Parser:
@@ -370,14 +404,29 @@ def __new__(cls):
370404
return cls._instance
371405

372406
def _process_registered_tool_modules(self):
373-
for module_name in RegisteredImports():
374-
try:
375-
importlib.import_module(module_name)
376-
except ImportError as exc:
377-
if os.environ.get("TOOLS_IGNORE_IMPORT_ERRORS", "0") == "0":
378-
self.context.warn(
379-
f"Could not import the registered tools module {module_name!r}: {exc}"
380-
)
407+
default_venv: VirtualEnv | ContextManager[None]
408+
default_venv_config = DefaultVirtualenvConfig().venv_config
409+
if default_venv_config:
410+
default_venv = VirtualEnv(ctx=self.context, **default_venv_config)
411+
else:
412+
default_venv = nullcontext()
413+
with default_venv:
414+
for module_name, venv_config in RegisteredImports():
415+
venv: VirtualEnv | ContextManager[None]
416+
if venv_config:
417+
if "name" not in venv_config:
418+
venv_config["name"] = module_name
419+
venv = VirtualEnv(ctx=self.context, **venv_config)
420+
else:
421+
venv = nullcontext()
422+
with venv:
423+
try:
424+
importlib.import_module(module_name)
425+
except ImportError as exc:
426+
if os.environ.get("TOOLS_IGNORE_IMPORT_ERRORS", "0") == "0":
427+
self.context.warn(
428+
f"Could not import the registered tools module {module_name!r}: {exc}"
429+
)
381430

382431
def parse_args(self):
383432
"""
@@ -471,6 +520,8 @@ def __init__(self, name, help, description=None, parent=None, venv_config=None):
471520
GroupReference.add_command(tuple(parent + [name]), self)
472521
parent = GroupReference()[tuple(parent)]
473522

523+
if venv_config and "name" not in venv_config:
524+
venv_config["name"] = self.name
474525
self.venv_config = venv_config or {}
475526
self.parser = parent.subparsers.add_parser(
476527
name.replace("_", "-"),
@@ -634,22 +685,22 @@ def __call__(self, func, options, venv_config: VirtualEnvConfig | None = None):
634685
kwargs[name] = getattr(options, name)
635686

636687
bound = signature.bind_partial(*args, **kwargs)
637-
venv = None
688+
venv: VirtualEnv | ContextManager[None]
638689
if venv_config:
639-
venv_name = getattr(options, f"{self.name}_command")
640-
venv = VirtualEnv(name=f"{self.name}.{venv_name}", ctx=self.context, **venv_config)
690+
if "name" not in venv_config:
691+
venv_config["name"] = getattr(options, f"{self.name}_command")
692+
venv = VirtualEnv(ctx=self.context, **venv_config)
641693
elif self.venv_config:
642-
venv = VirtualEnv(name=self.name, ctx=self.context, **self.venv_config)
643-
if venv:
644-
with venv:
645-
previous_venv = self.context.venv
646-
try:
647-
self.context.venv = venv
648-
func(self.context, *bound.args, **bound.kwargs)
649-
finally:
650-
self.context.venv = previous_venv
694+
venv = VirtualEnv(ctx=self.context, **self.venv_config)
651695
else:
652-
func(self.context, *bound.args, **bound.kwargs)
696+
venv = nullcontext()
697+
with venv:
698+
previous_venv = self.context.venv
699+
try:
700+
self.context.venv = venv
701+
func(self.context, *bound.args, **bound.kwargs)
702+
finally:
703+
self.context.venv = previous_venv
653704

654705

655706
def command_group(

src/ptscripts/virtualenv.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import pathlib
88
import shutil
9+
import site
910
import subprocess
1011
import sys
1112
import textwrap
@@ -32,12 +33,14 @@ class VirtualEnvConfig(TypedDict):
3233
Virtualenv Configuration Typing.
3334
"""
3435

36+
name: NotRequired[str]
3537
requirements: NotRequired[list[str]]
3638
requirements_files: NotRequired[list[pathlib.Path]]
3739
env: NotRequired[dict[str, str]]
3840
system_site_packages: NotRequired[bool]
3941
pip_requirement: NotRequired[str]
4042
setuptools_requirement: NotRequired[str]
43+
add_as_extra_site_packages: NotRequired[bool]
4144

4245

4346
def _cast_to_pathlib_path(value):
@@ -60,6 +63,7 @@ class VirtualEnv:
6063
system_site_packages: bool = attr.ib(default=False)
6164
pip_requirement: str = attr.ib(repr=False)
6265
setuptools_requirement: str = attr.ib(repr=False)
66+
add_as_extra_site_packages: bool = attr.ib(default=False)
6367
environ: dict[str, str] = attr.ib(init=False, repr=False)
6468
venv_dir: pathlib.Path = attr.ib(init=False)
6569
venv_python: pathlib.Path = attr.ib(init=False, repr=False)
@@ -175,6 +179,25 @@ def _create_virtualenv(self):
175179
self.setuptools_requirement,
176180
)
177181

182+
def _add_as_extra_site_packages(self):
183+
if self.add_as_extra_site_packages is False:
184+
return
185+
ret = self.run_code(
186+
"import json,site; print(json.dumps(site.getsitepackages()))",
187+
capture=True,
188+
check=False,
189+
)
190+
if ret.returncode:
191+
self.ctx.error(
192+
f"Failed to get the virtualenv's site packages path: {ret.stderr.decode()}"
193+
)
194+
self.ctx.exit(1)
195+
site_packages = site.getsitepackages()
196+
for path in json.loads(ret.stdout.strip().decode()):
197+
if path not in site_packages:
198+
site.addsitedir(path)
199+
site_packages = site.getsitepackages()
200+
178201
def __enter__(self):
179202
"""
180203
Creates the virtual environment when entering context.
@@ -184,6 +207,7 @@ def __enter__(self):
184207
except subprocess.CalledProcessError:
185208
raise AssertionError("Failed to create virtualenv")
186209
self._install_requirements()
210+
self._add_as_extra_site_packages()
187211
return self
188212

189213
def __exit__(self, *args):

0 commit comments

Comments
 (0)