Skip to content

Commit

Permalink
Add parse_config()
Browse files Browse the repository at this point in the history
Closes #40
  • Loading branch information
brettcannon committed Sep 18, 2023
1 parent d191688 commit bf20549
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 28 deletions.
28 changes: 28 additions & 0 deletions microvenv/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pathlib
import sys

# Exported as part of the public API.
Expand All @@ -6,3 +7,30 @@
# https://docs.python.org/3/library/venv.html#how-venvs-work
IN_VIRTUAL_ENV = sys.prefix != sys.base_prefix



def parse_config(env_dir):
"""Parse the pyvenv.cfg file in the specified virtual environment.
A dict is returned with strings for keys and values. All keys are
lowercased, but otherwise no validation is performed. No changes are made to
the values (e.g., include-system-site-packages is not converted to a boolean
nor lowerased).
Parsing is done in a way identical to how the 'site' modules does it. This
means that ANY line with an ``=`` sign is considered a line with a key/value
pair on it. As such, all other lines are treated as if they are comments.
But this also means that having a line start with e.g., ``#`` does not
signify a comment either if there is a ``=`` in the line.
"""
config = {}
venv_path = pathlib.Path(env_dir)
with open(venv_path / "pyvenv.cfg", "r", encoding="utf-8") as file:
# This is how `site` parses `pyvenv.cfg`, so it's about as
# official as we can get.
for line in file:
if "=" in line:
key, _, value = line.partition("=")
config[key.strip().lower()] = value.strip()
return config

10 changes: 10 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import pathlib
import sys
import venv

import pytest


@pytest.fixture
def executable():
"""Return the current interpreter's path."""
return pathlib.Path(sys.executable)


@pytest.fixture(scope="session")
def full_venv(tmp_path_factory):
"""Create a virtual environment via venv."""
venv_path = tmp_path_factory.mktemp("venvs") / "full_venv"
venv.create(venv_path, symlinks=True, with_pip=False, system_site_packages=False)
return venv_path
37 changes: 9 additions & 28 deletions tests/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,13 @@ def base_executable():
return pathlib.Path(sys.executable)


@pytest.fixture(scope="session")
def full_venv(tmp_path_factory):
venv_path = tmp_path_factory.mktemp("venvs") / "full_venv"
venv.create(venv_path, symlinks=True, with_pip=False, system_site_packages=False)
return venv_path


@pytest.fixture(scope="session")
def micro_venv(tmp_path_factory):
venv_path = tmp_path_factory.mktemp("venvs") / "micro_venv"
microvenv.create(venv_path)
return venv_path


def pyvenvcfg(venv_path):
config = {}
with open(venv_path / "pyvenv.cfg", "r", encoding="utf-8") as file:
for line in file:
if "=" in line:
# This is how `site` reads a `pyvenv.cfg`, so it's about as
# official as we can get.
key, _, value = line.partition("=")
config[key.strip().lower()] = value.strip()
return config


def test_code_size(executable, monkeypatch, tmp_path):
"""Make sure the source code can fit into `argv` for use with `-c`."""
with open(microvenv._create.__file__, "r", encoding="utf-8") as file:
Expand All @@ -56,7 +37,7 @@ def test_code_size(executable, monkeypatch, tmp_path):
# validating the virtual environment details as the CLI tests take care of
# that.
assert env_path.is_dir()
command = pyvenvcfg(env_path)["command"]
command = microvenv.parse_config(env_path)["command"]
assert command.startswith(sys.executable)
assert " -c " in command

Expand Down Expand Up @@ -101,8 +82,8 @@ def test_lib64(full_venv, micro_venv):
["include-system-site-packages", "version"],
)
def test_pyvenvcfg_data(full_venv, micro_venv, key):
full_config = pyvenvcfg(full_venv)
micro_config = pyvenvcfg(micro_venv)
full_config = microvenv.parse_config(full_venv)
micro_config = microvenv.parse_config(micro_venv)

# Use the full config as source of keys to check as as we may have keys in the micro
# venv that too new for the version of Python being tested against.
Expand All @@ -112,8 +93,8 @@ def test_pyvenvcfg_data(full_venv, micro_venv, key):


def test_pyvenvcfg_home(base_executable, full_venv, micro_venv):
full_config = pyvenvcfg(full_venv)
micro_config = pyvenvcfg(micro_venv)
full_config = microvenv.parse_config(full_venv)
micro_config = microvenv.parse_config(micro_venv)

assert full_config["home"] == os.fsdecode(base_executable.parent) # Sanity check.
assert micro_config["home"] == os.fsdecode(base_executable.parent)
Expand All @@ -122,19 +103,19 @@ def test_pyvenvcfg_home(base_executable, full_venv, micro_venv):
def test_pyvenvcfg_executable(base_executable, full_venv, micro_venv):
resolved_base_executable = base_executable.resolve()
executable_path = os.fsdecode(resolved_base_executable)
full_config = pyvenvcfg(full_venv)
full_config = microvenv.parse_config(full_venv)
if "executable" not in full_config:
# Introduced in Python 3.11.
pytest.skip("`executable` key not in pyvenv.cfg")

micro_config = pyvenvcfg(micro_venv)
micro_config = microvenv.parse_config(micro_venv)

assert full_config["executable"] == executable_path # Sanity check.
assert micro_config["executable"] == executable_path


def test_pyvenvcfg_command(executable, micro_venv):
config = pyvenvcfg(micro_venv)
config = microvenv.parse_config(micro_venv)
script_path = pathlib.Path(microvenv._create.__file__).resolve()
assert config["command"] == f"{executable} {script_path} {micro_venv.resolve()}"

Expand All @@ -144,5 +125,5 @@ def test_pyvenvcfg_command_relative(executable, monkeypatch, tmp_path):
venv_path = tmp_path / "venv"
microvenv.create(pathlib.Path(venv_path.name))
script_path = pathlib.Path(microvenv._create.__file__).resolve()
config = pyvenvcfg(venv_path)
config = microvenv.parse_config(venv_path)
assert config["command"] == f"{executable} {script_path} {venv_path.resolve()}"
46 changes: 46 additions & 0 deletions tests/test_parse_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import contextlib

import pytest

import microvenv


@contextlib.contextmanager
def write_config(venv_path, data):
"""Context manager to write the pyvenv.cfg and then restore it."""
config_path = venv_path / "pyvenv.cfg"
original_config = config_path.read_text(encoding="utf-8")
config_path.write_text(data, encoding="utf-8")
try:
yield config_path
finally:
config_path.write_text(original_config, encoding="utf-8")


@pytest.mark.parametrize("equals", ["=", " = ", "= ", " ="])
def test_formatting_around_equals(full_venv, equals):
with write_config(full_venv, f"key{equals}value\n"):
config = microvenv.parse_config(full_venv)

assert config["key"] == "value"


def test_comments(full_venv):
with write_config(full_venv, "# A comment\nkey = value\n"):
config = microvenv.parse_config(full_venv)

assert config["key"] == "value"


def test_multiple_equals(full_venv):
with write_config(full_venv, "key = value=value\n"):
config = microvenv.parse_config(full_venv)

assert config["key"] == "value=value"


def test_lowercase_keys(full_venv):
with write_config(full_venv, "Key = value\n"):
config = microvenv.parse_config(full_venv)

assert config["key"] == "value"

0 comments on commit bf20549

Please sign in to comment.