diff --git a/docs/changelog/1585.bugfix.rst b/docs/changelog/1585.bugfix.rst new file mode 100644 index 000000000..91cb7744d --- /dev/null +++ b/docs/changelog/1585.bugfix.rst @@ -0,0 +1 @@ +Expose a programmatic API as ``from virtualenv import cli_run`` - by :user:`gaborbernat`. diff --git a/docs/changelog/1585.doc.rst b/docs/changelog/1585.doc.rst new file mode 100644 index 000000000..47a865cd7 --- /dev/null +++ b/docs/changelog/1585.doc.rst @@ -0,0 +1,2 @@ +Document a programmatic API as ``from virtualenv import cli_run`` under :ref:`programmatic_api` - +by :user:`gaborbernat`. diff --git a/docs/render_cli.py b/docs/render_cli.py index af350d18f..4417c63d3 100644 --- a/docs/render_cli.py +++ b/docs/render_cli.py @@ -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 diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 32b523afe..fa178e74b 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -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 `_ 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: diff --git a/src/virtualenv/__init__.py b/src/virtualenv/__init__.py index 127d67ce7..c72643f02 100644 --- a/src/virtualenv/__init__.py +++ b/src/virtualenv/__init__.py @@ -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", +) diff --git a/src/virtualenv/__main__.py b/src/virtualenv/__main__.py index b7508203d..38fee15b7 100644 --- a/src/virtualenv/__main__.py +++ b/src/virtualenv/__main__.py @@ -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, @@ -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: diff --git a/src/virtualenv/config/ini.py b/src/virtualenv/config/ini.py index 017aae93f..883960706 100644 --- a/src/virtualenv/config/ini.py +++ b/src/virtualenv/config/ini.py @@ -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") @@ -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: diff --git a/src/virtualenv/run/__init__.py b/src/virtualenv/run/__init__.py index ed7f6ef9b..443ba669a 100644 --- a/src/virtualenv/run/__init__.py +++ b/src/virtualenv/run/__init__.py @@ -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() diff --git a/src/virtualenv/session.py b/src/virtualenv/session.py index 5f655c321..9e799668f 100644 --- a/src/virtualenv/session.py +++ b/src/virtualenv/session.py @@ -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() diff --git a/tests/conftest.py b/tests/conftest.py index 8dc6a1cae..777dec5ac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 diff --git a/tests/integration/test_zipapp.py b/tests/integration/test_zipapp.py index ed4f6ef4e..152000dab 100644 --- a/tests/integration/test_zipapp.py +++ b/tests/integration/test_zipapp.py @@ -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 @@ -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) @@ -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)) diff --git a/tests/unit/activation/conftest.py b/tests/unit/activation/conftest.py index 17f028dc8..f0ddfd8a4 100644 --- a/tests/unit/activation/conftest.py +++ b/tests/unit/activation/conftest.py @@ -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 @@ -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 diff --git a/tests/unit/create/conftest.py b/tests/unit/create/conftest.py index 07e821f45..3b2aada47 100644 --- a/tests/unit/create/conftest.py +++ b/tests/unit/create/conftest.py @@ -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 @@ -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( diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py index 919679af1..6af52062e 100644 --- a/tests/unit/create/test_creator.py +++ b/tests/unit/create/test_creator.py @@ -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() @@ -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 @@ -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 @@ -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() @@ -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 @@ -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: @@ -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 @@ -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"]) diff --git a/tests/unit/create/test_interpreters.py b/tests/unit/create/test_interpreters.py index d14bf0cd8..002326b8b 100644 --- a/tests/unit/create/test_interpreters.py +++ b/tests/unit/create/test_interpreters.py @@ -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 @@ -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())) ) diff --git a/tests/unit/seed/test_boostrap_link_via_app_data.py b/tests/unit/seed/test_boostrap_link_via_app_data.py index 65620bd74..87bace139 100644 --- a/tests/unit/seed/test_boostrap_link_via_app_data.py +++ b/tests/unit/seed/test_boostrap_link_via_app_data.py @@ -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 @@ -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 @@ -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()) diff --git a/tests/unit/seed/test_extra_install.py b/tests/unit/seed/test_extra_install.py index 14898fa37..dd3bc238b 100644 --- a/tests/unit/seed/test_extra_install.py +++ b/tests/unit/seed/test_extra_install.py @@ -6,7 +6,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 @@ -36,7 +36,7 @@ def builtin_shows_marker_missing(): ) @pytest.mark.parametrize("creator", list(i for i in CREATOR_CLASSES.keys() if i != "builtin")) def test_can_build_c_extensions(creator, tmp_path, coverage_env): - session = run_via_cli(["--creator", creator, "--seed", "app-data", str(tmp_path), "-vvv"]) + session = cli_run(["--creator", creator, "--seed", "app-data", str(tmp_path), "-vvv"]) coverage_env() cmd = [ str(session.creator.script("pip")), diff --git a/tests/unit/seed/test_pip_invoke.py b/tests/unit/seed/test_pip_invoke.py index 0795a9f7e..6cd71147d 100644 --- a/tests/unit/seed/test_pip_invoke.py +++ b/tests/unit/seed/test_pip_invoke.py @@ -3,7 +3,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.seed.embed.wheels import BUNDLE_SUPPORT @@ -22,7 +22,7 @@ def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env, current_fastest): "--creator", current_fastest, ] - result = run_via_cli(create_cmd) + result = cli_run(create_cmd) coverage_env() assert result diff --git a/tests/unit/test_run.py b/tests/unit/test_run.py index 133dfdb39..436e5eba6 100644 --- a/tests/unit/test_run.py +++ b/tests/unit/test_run.py @@ -4,12 +4,12 @@ import six from virtualenv import __version__ -from virtualenv.run import run_via_cli +from virtualenv.run import cli_run def test_help(capsys): with pytest.raises(SystemExit) as context: - run_via_cli(args=["-h", "-vvv"]) + cli_run(args=["-h", "-vvv"]) assert context.value.code == 0 out, err = capsys.readouterr() @@ -19,7 +19,7 @@ def test_help(capsys): def test_version(capsys): with pytest.raises(SystemExit) as context: - run_via_cli(args=["--version"]) + cli_run(args=["--version"]) assert context.value.code == 0 out, err = capsys.readouterr()