Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
devds96 committed Oct 21, 2023
0 parents commit f11d0b7
Show file tree
Hide file tree
Showing 32 changed files with 3,288 additions and 0 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Tests

on:
- push
- pull_request

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
python-version: [
"3.7", "3.8", "3.9", "3.10", "3.11", "3.12"
]
steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Run tests with tox
run: tox
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
__pycache__
.coverage
.DS_Store
.hypothesis
.mypy_cache
.pytest_cache
.tox
*.egg-info
build
venv_devel@venvfromfile
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 devds96 (contact.devds96@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# venvfromfile

![Tests](https://github.com/devds96/venvfromfile/actions/workflows/tests.yml/badge.svg)

A simple runnable Python package that sets up virtual environments as
specified in a configuration file using the builtin `venv` module.


## Examples
The configuration files for the construction of virtual environments are
`.yaml` files like the following example (compare `venv_devel.yaml`
in the root directory of this repo):
```
min_version: ">=3.7"
venv_configs:
- directory: venv@venvfromfile
requirement_files:
- requirements.txt
pth_paths:
- src
```
This constructs a virtual environment next to the configuration file if
at least Python 3.7 is used. The directory will be named
"venv@venvfromfile" and requirements from a file "requirements.txt"
which should be placed next to the configuration file will be installed
in the virtual environment.
Additionally, a file ".pth" is installed in the virtual environment
containing the path to the directory "src" relative to the configuration
file. This instructs Python to search this path for installed packages
when importing. More information on `.pth` files can be found in the
[documentation of the `site`](https://docs.python.org/3/library/site.html)
module (builtin).

Note that all relative paths specified in the configuration files are
interpreted relative to the configuration file. This includes, for
example the directory names of the virtual environment.

For further information on the available options see the classes
contained in the `conf.py` module and their fields' docstrings.

In order to construct the virtual environment specified by the file
above, the package can be invoked as
```
python -m venvfromfile filename.yaml
```
where `filename.yaml` is the file name of the configuration file.

For further information on available command line options type
```
python -m venvfromfile -h
```

Another simple example:
```
min_version: ">=3.7"
venv_configs:
- directory: py37
max_version: "<=3.7"
- directory: py38_39
min_version: ">=3.8"
max_version: "<3.9"
```
This would construct a plain virtual environment without any
requirements in the "py37" directory if Python version 3.7 or below
is used and in the "py38_39" directory if the Python version is above
3.8 and strictly below 3.9. Note that the first comparison will only
hold for Python version 3.7 exactly, since the entire config only
applies to Python versions 3.7 and above.


## Installation
You can install this package directly from git:
```
pip install git+https://github.com/devds96/venvfromfile
```

Alternatively, clone the git repo and run in its root directory:
```
pip install .
```
61 changes: 61 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[build-system]
requires = ["setuptools>=42.0"]
build-backend = "setuptools.build_meta"

[project]
name = "venvfromfile"
authors = [
{name = "devds96", email = "src.devds96@gmail.com"}
]
license = {text = "MIT"}
description = "Construct a virtual environment (venv) from a configuration file."
requires-python = ">=3.7"
classifiers = [
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Utilities",
"Typing :: Typed",
]
dynamic = [
"dependencies",
"readme",
"version",
"optional-dependencies"
]

[tool.setuptools.dynamic]
version = {attr = "venvfromfile.__version__"}
readme = {file = ["README.md"]}
dependencies = {file = ["requirements.txt"]}
optional-dependencies = {tests = { file = ["requirements_tests.txt"] }}

[tool.pytest.ini_options]
addopts = "--cov=venvfromfile --cov-report term-missing"
testpaths = [
"tests",
]

[tool.coverage.run]
branch = true
source = [
"venvfromfile"
]
omit = [
"*/venvfromfile/__main__.py",
"*/venvfromfile/__init__.py",
"*/venvfromfile/builder.py",
"*/venvfromfile/conf.py"
]

[tool.coverage.report]
exclude_also = [
"@_?overload"
]
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pydantic
pydantic-yaml
typing_extensions;python_version<"3.11"
9 changes: 9 additions & 0 deletions requirements_tests.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
hypothesis
hypothesis-fspaths
more_itertools
pydantic
pydantic-yaml
pytest
pytest-cov
tox
typing_extensions;python_version<"3.11"
164 changes: 164 additions & 0 deletions src/venvfromfile/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"""This module can be run to create a virtual environment (venv) from a
config file.
"""

__all__ = [
"EnvBuilder", "EnvContext",
"BaseVenvConfig", "VenvConfig", "VenvConfigExtraDef", "VenvConfigRoot",
"PyVersionError", "UnsupportedArgument", "UseSymlinksException"
]

__author__ = "devds96"
__email__ = "src.devds96@gmail.com"
__license__ = "MIT"
__version__ = "0.1.0"

import logging as _logging
import os.path as _ospath
import pydantic_yaml as _pydantic_yaml
import sys as _sys

if _sys.version_info >= (3, 8):
from typing import Literal as _Literal
else:
from typing_extensions import Literal as _Literal

from typing import List as _List, Optional as _Optional, \
overload as _overload, Sequence as _Sequence, Tuple as _Tuple, \
Union as _Union

from . import _pyver

from .builder import EnvBuilder, EnvContext
from .conf import BaseVenvConfig, VenvConfig, VenvConfigExtraDef, \
VenvConfigRoot
from ._exceptions import PyVersionError, UnsupportedArgument, \
UseSymlinksException


_logger = _logging.getLogger(__name__)
"""The logger for this module."""


def load_conf_from_file(path: str) -> VenvConfigRoot:
"""Load the configuration root from a file.
Args:
path (str): The file to load.
Returns:
VenvConfigRoot: The loaded config root.
"""
res = _pydantic_yaml.parse_yaml_file_as(VenvConfigRoot, path)
_logger.debug("The deserialized config root is %s", res)
return res


def build_conf(conf_path: str, conf: VenvConfig) -> EnvBuilder:
"""Build the config specified by a file.
Args:
conf_path (str): The path to the config file.
conf (VenvConfig): The configuration.
Returns:
builder.EnvBuilder: The builder after the construction
completes.
"""
_logger.info(f"Building environment {conf.directory!r}.")
_logger.debug(f"Building from config {conf!r}.")
b = EnvBuilder(conf_path, conf)
b.create(conf.directory)
return b


@_overload
def main(
conf_file_path: _Sequence[str],
*,
ret_builders: _Union[_Literal[False], None] = None
) -> int:
pass


@_overload
def main(
conf_file_path: _Sequence[str],
*,
ret_builders: _Literal[True]
) -> _Tuple[int, _Sequence[EnvBuilder]]:
pass


def main(
conf_file_paths: _Sequence[str],
*,
ret_builders: _Optional[bool] = None
) -> _Union[int, _Tuple[int, _Sequence[EnvBuilder]]]:
"""The main entry point for the construction of virtual environments
based on configuration data.
Args:
conf_file_paths (_Sequence[str]): The paths to the configuration
files.
ret_builders (Optional[bool], optional): Whether or not to
return the used `EnvBuilder` instances. Defaults to False.
Raises:
TypeError: If `conf_file_paths` is a str instance instead of a
Sequence[str].
Returns:
int | tuple[int, Sequence[EnvBuilder]]]: The return code (0) or
the return code followed by a Sequence[EnvBuilder]
representing the used environment builders.
"""

if isinstance(conf_file_paths, str):
# This prevents typos.
raise TypeError(
"'conf_file_paths' was of type str. Expected Sequence[str]."
)

builders: _List[EnvBuilder] = list()

for sf in conf_file_paths:
if not _ospath.isabs(sf):
sf = _ospath.abspath(sf)
_logger.info(f"Loading configuration file {sf!r}.")
conf = load_conf_from_file(sf)
try:
conf.ensure_pyversion_compatible()
except PyVersionError as pve:
_logger.warning(pve)
_logger.warning("Skipping this config file.")
continue

cv = _sys.version_info

for i, venv_config in enumerate(conf.venv_configs):
dir_name = venv_config.directory
if not venv_config.is_pyversion_compatible():
_logger.info(
f"The environment at index {i} with directory "
f"{dir_name!r} is incompatible with the "
"current Python version "
f"({_pyver.format_version_info(cv)}) and will be "
"skipped."
)
continue
b = build_conf(sf, venv_config)
builders.append(b)

num_ok = len(builders)
if num_ok > 0:
s = "s have" if num_ok > 1 else " has"
_logger.info(
f"{num_ok} compatible virtual environment{s} been set up."
)
else:
_logger.warning("No virtual environments have been set up.")

if ret_builders is True:
return 0, tuple(builders)
return 0
Loading

0 comments on commit f11d0b7

Please sign in to comment.