Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expose and document programmatic API #1592

Merged
merged 1 commit into from
Feb 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog/1585.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Expose a programmatic API as ``from virtualenv import cli_run`` - by :user:`gaborbernat`.
2 changes: 2 additions & 0 deletions docs/changelog/1585.doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Document a programmatic API as ``from virtualenv import cli_run`` under :ref:`programmatic_api` -
by :user:`gaborbernat`.
2 changes: 1 addition & 1 deletion docs/render_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def build_rows(options):
names = option["name"]
default = option["default"]
if default is not None:
if isinstance(default, str) and default[0] == default[-1] and default[0] == '"':
if isinstance(default, str) and default and default[0] == default[-1] and default[0] == '"':
default = default[1:-1]
if default == SUPPRESS:
default = None
Expand Down
24 changes: 24 additions & 0 deletions docs/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,27 @@ also provisions a ``decativate`` command that will allow you to undo the operati
A longer explanation of this can be found within Allison Kaptur's 2013 blog post: `There's no magic: virtualenv
edition <https://www.recurse.com/blog/14-there-is-no-magic-virtualenv-edition>`_ explains how virtualenv uses bash and
Python and ``PATH`` and ``PYTHONHOME`` to isolate virtual environments' paths.

.. _programmatic_api:

Programmatic API
----------------

At the moment ``virtualenv`` offers only CLI level interface. If you want to trigger invocation of Python environments
from within Python you should be using the ``virtualenv.cli_run`` method; this takes an ``args`` argument where you can
pass the options the same way you would from the command line. The run will return a session object containing data
about the created virtual environment.

.. code-block:: python

from virtualenv import cli_run

cli_run(["venv"])

.. automodule:: virtualenv
:members:

.. currentmodule:: virtualenv.session

.. autoclass:: Session
:members:
6 changes: 5 additions & 1 deletion src/virtualenv/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from __future__ import absolute_import, unicode_literals

from .run import cli_run
from .version import __version__

__all__ = ("__version__", "run")
__all__ = (
"__version__",
"cli_run",
)
6 changes: 3 additions & 3 deletions src/virtualenv/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
def run(args=None, options=None):
start = datetime.now()
from virtualenv.error import ProcessCallFailed
from virtualenv.run import run_via_cli
from virtualenv.run import cli_run

if args is None:
args = sys.argv[1:]
try:
session = run_via_cli(args, options)
session = cli_run(args, options)
logging.warning(
"created virtual environment in %.0fms %s with seeder %s",
(datetime.now() - start).total_seconds() * 1000,
Expand All @@ -35,7 +35,7 @@ def run_with_catch(args=None):
try:
run(args, options)
except (KeyboardInterrupt, Exception) as exception:
if options.with_traceback:
if getattr(options, "with_traceback", False):
logging.shutdown() # force flush of log messages before the trace is printed
raise
else:
Expand Down
32 changes: 19 additions & 13 deletions src/virtualenv/config/ini.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@

from .convert import convert

DEFAULT_CONFIG_FILE = default_config_dir() / "virtualenv.ini"


class IniConfig(object):
VIRTUALENV_CONFIG_FILE_ENV_VAR = six.ensure_str("VIRTUALENV_CONFIG_FILE")
Expand All @@ -24,18 +22,26 @@ class IniConfig(object):
def __init__(self):
config_file = os.environ.get(self.VIRTUALENV_CONFIG_FILE_ENV_VAR, None)
self.is_env_var = config_file is not None
self.config_file = Path(config_file) if config_file is not None else DEFAULT_CONFIG_FILE
self.config_file = Path(config_file) if config_file is not None else (default_config_dir() / "virtualenv.ini")
self._cache = {}
self.has_config_file = self.config_file.exists()
if self.has_config_file:
self.config_file = self.config_file.resolve()
self.config_parser = ConfigParser.ConfigParser()
try:
self._load()
self.has_virtualenv_section = self.config_parser.has_section(self.section)
except Exception as exception:
logging.error("failed to read config file %s because %r", config_file, exception)
self.has_config_file = None

exception = None
self.has_config_file = None
try:
self.has_config_file = self.config_file.exists()
except OSError as exc:
exception = exc
else:
if self.has_config_file:
self.config_file = self.config_file.resolve()
self.config_parser = ConfigParser.ConfigParser()
try:
self._load()
self.has_virtualenv_section = self.config_parser.has_section(self.section)
except Exception as exc:
exception = exc
if exception is not None:
logging.error("failed to read config file %s because %r", config_file, exception)

def _load(self):
with self.config_file.open("rt") as file_handler:
Expand Down
7 changes: 4 additions & 3 deletions src/virtualenv/run/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
from .plugin.seeders import SeederSelector


def run_via_cli(args, options=None):
"""Run the virtual environment creation via CLI arguments
def cli_run(args, options=None):
"""Create a virtual environment given some command line interface arguments

:param args: the command line arguments
:return: the creator used
:param options: passing in a ``argparse.Namespace`` object allows return of the parsed options
:return: the session object of the creation (its structure for now is experimental and might change on short notice)
"""
session = session_via_cli(args, options)
session.run()
Expand Down
37 changes: 32 additions & 5 deletions src/virtualenv/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,39 @@


class Session(object):
"""Represents a virtual environment creation session"""

def __init__(self, verbosity, interpreter, creator, seeder, activators):
self.verbosity = verbosity
self.interpreter = interpreter
self.creator = creator
self.seeder = seeder
self.activators = activators
self._verbosity = verbosity
self._interpreter = interpreter
self._creator = creator
self._seeder = seeder
self._activators = activators

@property
def verbosity(self):
"""The verbosity of the run"""
return self._verbosity

@property
def interpreter(self):
"""Create a virtual environment based on this reference interpreter"""
return self._interpreter

@property
def creator(self):
"""The creator used to build the virtual environment (must be compatible with the interpreter)"""
return self._creator

@property
def seeder(self):
"""The mechanism used to provide the seed packages (pip, setuptools, wheel)"""
return self._seeder

@property
def activators(self):
"""Activators used to generate activations scripts"""
return self._activators

def run(self):
self._create()
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ def ensure_py_info_cache_empty():


@pytest.fixture(autouse=True)
def ignore_global_config(tmp_path, mocker):
def ignore_global_config(tmp_path, mocker, monkeypatch):
mocker.patch("virtualenv.dirs._CFG_DIR", None)
mocker.patch("virtualenv.dirs.user_config_dir", return_value=tmp_path / "this-should-never-exist")
mocker.patch("virtualenv.dirs.user_config_dir", return_value=Path(str(tmp_path / "this-should-never-exist")))
yield


Expand Down
6 changes: 3 additions & 3 deletions tests/integration/test_zipapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import six

from virtualenv.discovery.py_info import PythonInfo
from virtualenv.run import run_via_cli
from virtualenv.run import cli_run
from virtualenv.util.path import Path

HERE = Path(__file__).parent
Expand All @@ -28,7 +28,7 @@ def zipapp_build_env(tmp_path_factory):
for version in range(8, 4, -1):
try:
# create a virtual environment which is also guaranteed to contain a recent enough pip (bundled)
session = run_via_cli(
session = cli_run(
["-vvv", "-p", "{}3.{}".format(impl, version), "--activators", "", str(create_env_path)]
)
exe = str(session.creator.exe)
Expand Down Expand Up @@ -61,7 +61,7 @@ def zipapp(zipapp_build_env, tmp_path_factory):
@pytest.fixture(scope="session")
def zipapp_test_env(tmp_path_factory):
base_path = tmp_path_factory.mktemp("zipapp-test")
session = run_via_cli(["-v", "--activators", "", "--without-pip", str(base_path / "env")])
session = cli_run(["-v", "--activators", "", "--without-pip", str(base_path / "env")])
yield session.creator.exe
shutil.rmtree(str(base_path))

Expand Down
4 changes: 2 additions & 2 deletions tests/unit/activation/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import six

from virtualenv.info import IS_PYPY, WIN_CPYTHON_2
from virtualenv.run import run_via_cli
from virtualenv.run import cli_run
from virtualenv.util.path import Path
from virtualenv.util.subprocess import Popen

Expand Down Expand Up @@ -207,7 +207,7 @@ def raise_on_non_source_class():
@pytest.fixture(scope="session")
def activation_python(tmp_path_factory, special_char_name, current_fastest):
dest = os.path.join(six.ensure_text(str(tmp_path_factory.mktemp("activation-tester-env"))), special_char_name)
session = run_via_cli(["--without-pip", dest, "--prompt", special_char_name, "--creator", current_fastest, "-vv"])
session = cli_run(["--without-pip", dest, "--prompt", special_char_name, "--creator", current_fastest, "-vv"])
pydoc_test = session.creator.purelib / "pydoc_test.py"
pydoc_test.write_text('"""This is pydoc_test.py"""')
yield session
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/create/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import pytest

from virtualenv.discovery.py_info import PythonInfo
from virtualenv.run import run_via_cli
from virtualenv.run import cli_run
from virtualenv.util.path import Path
from virtualenv.util.subprocess import Popen

Expand Down Expand Up @@ -45,7 +45,7 @@ def old_virtualenv(tmp_path_factory):
return CURRENT.executable
else:
env_for_old_virtualenv = tmp_path_factory.mktemp("env-for-old-virtualenv")
result = run_via_cli(["--no-download", "--activators", "", str(env_for_old_virtualenv)])
result = cli_run(["--no-download", "--activators", "", str(env_for_old_virtualenv)])
# noinspection PyBroadException
try:
process = Popen(
Expand Down
20 changes: 10 additions & 10 deletions tests/unit/create/test_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
import pytest
import six

from virtualenv.__main__ import run
from virtualenv.__main__ import run, run_with_catch
from virtualenv.create.creator import DEBUG_SCRIPT, Creator, get_env_debug_info
from virtualenv.discovery.builtin import get_interpreter
from virtualenv.discovery.py_info import PythonInfo
from virtualenv.info import IS_PYPY, fs_supports_symlink
from virtualenv.pyenv_cfg import PyEnvCfg
from virtualenv.run import run_via_cli, session_via_cli
from virtualenv.run import cli_run, session_via_cli
from virtualenv.util.path import Path

CURRENT = PythonInfo.current_system()
Expand All @@ -38,7 +38,7 @@ def test_os_path_sep_not_allowed(tmp_path, capsys, sep):

def _non_success_exit_code(capsys, target):
with pytest.raises(SystemExit) as context:
run_via_cli(args=[target])
run_with_catch(args=[target])
assert context.value.code != 0
out, err = capsys.readouterr()
assert not out, out
Expand Down Expand Up @@ -126,7 +126,7 @@ def test_create_no_seed(python, creator, isolated, system, coverage_env, special
]
if isolated == "global":
cmd.append("--system-site-packages")
result = run_via_cli(cmd)
result = cli_run(cmd)
coverage_env()
if IS_PYPY:
# pypy cleans up file descriptors periodically so our (many) subprocess calls impact file descriptor limits
Expand Down Expand Up @@ -202,7 +202,7 @@ def _session_via_cli(args, options=None):
@pytest.mark.skipif(not sys.version_info[0] == 2, reason="python 2 only tests")
def test_debug_bad_virtualenv(tmp_path):
cmd = [str(tmp_path), "--without-pip"]
result = run_via_cli(cmd)
result = cli_run(cmd)
# if the site.py is removed/altered the debug should fail as no one is around to fix the paths
site_py = result.creator.stdlib / "site.py"
site_py.unlink()
Expand All @@ -223,12 +223,12 @@ def test_create_clear_resets(tmp_path, creator, clear, caplog):
pytest.skip("venv without clear might fail")
marker = tmp_path / "magic"
cmd = [str(tmp_path), "--seeder", "app-data", "--without-pip", "--creator", creator, "-vvv"]
run_via_cli(cmd)
cli_run(cmd)

marker.write_text("") # if we a marker file this should be gone on a clear run, remain otherwise
assert marker.exists()

run_via_cli(cmd + (["--clear"] if clear else []))
cli_run(cmd + (["--clear"] if clear else []))
assert marker.exists() is not clear


Expand All @@ -239,7 +239,7 @@ def test_prompt_set(tmp_path, creator, prompt):
if prompt is not None:
cmd.extend(["--prompt", "magic"])

result = run_via_cli(cmd)
result = cli_run(cmd)
actual_prompt = tmp_path.name if prompt is None else prompt
cfg = PyEnvCfg.from_file(result.creator.pyenv_cfg.path)
if prompt is None:
Expand Down Expand Up @@ -276,7 +276,7 @@ def test_cross_major(cross_python, coverage_env, tmp_path, current_fastest):
"--creator",
current_fastest,
]
result = run_via_cli(cmd)
result = cli_run(cmd)
coverage_env()
env = PythonInfo.from_exe(str(result.creator.exe))
assert env.version_info.major != CURRENT.version_info.major
Expand Down Expand Up @@ -312,5 +312,5 @@ def test_create_long_path(current_fastest, tmp_path):
folder.mkdir(parents=True)

cmd = [str(folder)]
result = run_via_cli(cmd)
result = cli_run(cmd)
subprocess.check_call([str(result.creator.script("pip")), "--version"])
6 changes: 3 additions & 3 deletions tests/unit/create/test_interpreters.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
import pytest

from virtualenv.discovery.py_info import PythonInfo
from virtualenv.run import run_via_cli
from virtualenv.run import cli_run


@pytest.mark.slow
def test_failed_to_find_bad_spec():
of_id = uuid4().hex
with pytest.raises(RuntimeError) as context:
run_via_cli(["-p", of_id])
cli_run(["-p", of_id])
msg = repr(RuntimeError("failed to find interpreter for Builtin discover of python_spec={!r}".format(of_id)))
assert repr(context.value) == msg

Expand All @@ -22,7 +22,7 @@ def test_failed_to_find_bad_spec():
def test_failed_to_find_implementation(of_id, mocker):
mocker.patch("virtualenv.run.plugin.creators.CreatorSelector._OPTIONS", return_value={})
with pytest.raises(RuntimeError) as context:
run_via_cli(["-p", of_id])
cli_run(["-p", of_id])
assert repr(context.value) == repr(
RuntimeError("No virtualenv implementation for {}".format(PythonInfo.current_system()))
)
6 changes: 3 additions & 3 deletions tests/unit/seed/test_boostrap_link_via_app_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from virtualenv.discovery.py_info import PythonInfo
from virtualenv.info import fs_supports_symlink
from virtualenv.run import run_via_cli
from virtualenv.run import cli_run
from virtualenv.seed.embed.wheels import BUNDLE_SUPPORT
from virtualenv.seed.embed.wheels.acquire import BUNDLE_FOLDER
from virtualenv.util.subprocess import Popen
Expand Down Expand Up @@ -37,7 +37,7 @@ def test_base_bootstrap_link_via_app_data(tmp_path, coverage_env, current_fastes
]
if not copies:
create_cmd.append("--symlink-app-data")
result = run_via_cli(create_cmd)
result = cli_run(create_cmd)
coverage_env()
assert result

Expand Down Expand Up @@ -82,7 +82,7 @@ def test_base_bootstrap_link_via_app_data(tmp_path, coverage_env, current_fastes
assert setuptools not in files_post_first_uninstall

# check we can run it again and will work - checks both overwrite and reuse cache
result = run_via_cli(create_cmd)
result = cli_run(create_cmd)
coverage_env()
assert result
files_post_second_create = list(site_package.iterdir())
Expand Down
Loading