diff --git a/README.md b/README.md index 11f3e1e..d0acc3e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,37 @@ Run any Python function, with any dependencies, in any machine you want. Isolate offers a pluggable end-to-end solution for building, managing, and using isolated environments (virtualenv, -conda, and possibly more). +conda, remote, and more). + +## Try it! + +```py +from isolate import Template, LocalBox + +# Build you first environment by specifying its kind (like virtualenv or conda) +template = Template("virtualenv") + +# Add some packages to it. +template << "pyjokes==0.5.0" + +# Forward it to a box (your local machine, or a remote machine) +environment = template >> LocalBox() + +# And then, finally try executing some code + +def get_pyjokes_version(): + import pyjokes + + return pyjokes.__version__ + +# If pyjokes==0.6.0 is installed in your local environment, it is going to print +# 0.6.0 here. +print("Installed pyjokes version: ", get_pyjokes_version()) + +# But if you run the same function in an isolated environment, you'll get +# 0.5.0. +print("Isolated pyjokes version: ", environment.run(get_pyjokes_version)) +``` ## Motivation diff --git a/poetry.lock b/poetry.lock index 1258fe6..1780815 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,22 +1,13 @@ [[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" category = "main" -optional = true -python-versions = ">=3.7" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +optional = false +python-versions = "*" -[[package]] -name = "colorama" -version = "0.4.5" -description = "Cross-platform colored terminal text." -category = "main" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "distlib" @@ -35,27 +26,22 @@ optional = false python-versions = ">=3.7" [package.extras] -testing = ["pytest-timeout (>=2.1)", "pytest-cov (>=3)", "pytest (>=7.1.2)", "coverage (>=6.4.2)", "covdefaults (>=2.2)"] -docs = ["sphinx-autodoc-typehints (>=1.19.1)", "sphinx (>=5.1.1)", "furo (>=2022.6.21)"] +docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] +testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] [[package]] -name = "flask" -version = "2.2.2" -description = "A simple framework for building complex web applications." +name = "grpcio" +version = "1.50.0" +description = "HTTP/2-based RPC framework" category = "main" -optional = true +optional = false python-versions = ">=3.7" [package.dependencies] -click = ">=8.0" -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} -itsdangerous = ">=2.0" -Jinja2 = ">=3.0" -Werkzeug = ">=2.2.2" +six = ">=1.5.2" [package.extras] -async = ["asgiref (>=3.2)"] -dotenv = ["python-dotenv"] +protobuf = ["grpcio-tools (>=1.50.0)"] [[package]] name = "importlib-metadata" @@ -75,99 +61,59 @@ perf = ["ipython"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] [[package]] -name = "itsdangerous" -version = "2.1.2" -description = "Safely pass data to untrusted environments and back." -category = "main" -optional = true -python-versions = ">=3.7" - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "main" -optional = true +optional = false python-versions = ">=3.7" -[package.dependencies] -MarkupSafe = ">=2.0" - [package.extras] -i18n = ["Babel (>=2.7)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] [[package]] -name = "markupsafe" -version = "2.1.1" -description = "Safely add untrusted strings to HTML/XML markup." +name = "protobuf" +version = "4.21.9" +description = "" category = "main" -optional = true +optional = false python-versions = ">=3.7" [[package]] -name = "marshmallow" -version = "3.18.0" -description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +name = "pygments" +version = "2.13.0" +description = "Pygments is a syntax highlighting package written in Python." category = "main" -optional = true -python-versions = ">=3.7" - -[package.dependencies] -packaging = ">=17.0" +optional = false +python-versions = ">=3.6" [package.extras] -dev = ["pytest", "pytz", "simplejson", "mypy (==0.971)", "flake8 (==5.0.4)", "flake8-bugbear (==22.9.11)", "pre-commit (>=2.4,<3.0)", "tox"] -docs = ["sphinx (==5.1.1)", "sphinx-issues (==3.0.1)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.9)"] -lint = ["mypy (==0.971)", "flake8 (==5.0.4)", "flake8-bugbear (==22.9.11)", "pre-commit (>=2.4,<3.0)"] -tests = ["pytest", "pytz", "simplejson"] +plugins = ["importlib-metadata"] [[package]] -name = "packaging" -version = "21.3" -description = "Core utilities for Python packages" -category = "main" -optional = true -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - -[[package]] -name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "rich" +version = "12.6.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false -python-versions = ">=3.7" - -[package.extras] -test = ["pytest (>=6)", "pytest-mock (>=3.6)", "pytest-cov (>=2.7)", "appdirs (==1.4.4)"] -docs = ["sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)", "proselint (>=0.10.2)", "furo (>=2021.7.5b38)"] +python-versions = ">=3.6.3,<4.0.0" -[[package]] -name = "pyjwt" -version = "2.5.0" -description = "JSON Web Token implementation in Python" -category = "main" -optional = true -python-versions = ">=3.7" +[package.dependencies] +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] -crypto = ["cryptography (>=3.3.1)", "types-cryptography (>=3.3.21)"] -dev = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1)", "types-cryptography (>=3.3.21)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "pre-commit"] -docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] [[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" category = "main" -optional = true -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["railroad-diagrams", "jinja2"] +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "typing-extensions" @@ -179,64 +125,47 @@ python-versions = ">=3.7" [[package]] name = "virtualenv" -version = "20.16.5" +version = "20.16.6" description = "Virtual Python Environment builder" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -distlib = ">=0.3.5,<1" +distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} platformdirs = ">=2.4,<3" [package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] -[[package]] -name = "werkzeug" -version = "2.2.2" -description = "The comprehensive WSGI web application library." -category = "main" -optional = true -python-versions = ">=3.7" - -[package.dependencies] -MarkupSafe = ">=2.1.1" - -[package.extras] -watchdog = ["watchdog"] - [[package]] name = "zipp" -version = "3.8.1" +version = "3.10.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "jaraco.functools", "more-itertools", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [extras] -server = ["flask", "marshmallow", "pyjwt"] +grpc = [] +server = [] [metadata] lock-version = "1.1" -python-versions = ">=3.7" -content-hash = "1cda82c58809824664cd521c4f654777e15e73d23de8004062c50ac8b0148a25" +python-versions = ">=3.7,<4.0" +content-hash = "5d2172c3d31c2f2de606884e3e6ff842c5aa82637bb33e227a073cde5161daac" [metadata.files] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, @@ -246,97 +175,98 @@ filelock = [ {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] -flask = [ - {file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"}, - {file = "Flask-2.2.2.tar.gz", hash = "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b"}, +grpcio = [ + {file = "grpcio-1.50.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:906f4d1beb83b3496be91684c47a5d870ee628715227d5d7c54b04a8de802974"}, + {file = "grpcio-1.50.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:2d9fd6e38b16c4d286a01e1776fdf6c7a4123d99ae8d6b3f0b4a03a34bf6ce45"}, + {file = "grpcio-1.50.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:4b123fbb7a777a2fedec684ca0b723d85e1d2379b6032a9a9b7851829ed3ca9a"}, + {file = "grpcio-1.50.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2f77a90ba7b85bfb31329f8eab9d9540da2cf8a302128fb1241d7ea239a5469"}, + {file = "grpcio-1.50.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eea18a878cffc804506d39c6682d71f6b42ec1c151d21865a95fae743fda500"}, + {file = "grpcio-1.50.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b71916fa8f9eb2abd93151fafe12e18cebb302686b924bd4ec39266211da525"}, + {file = "grpcio-1.50.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:95ce51f7a09491fb3da8cf3935005bff19983b77c4e9437ef77235d787b06842"}, + {file = "grpcio-1.50.0-cp310-cp310-win32.whl", hash = "sha256:f7025930039a011ed7d7e7ef95a1cb5f516e23c5a6ecc7947259b67bea8e06ca"}, + {file = "grpcio-1.50.0-cp310-cp310-win_amd64.whl", hash = "sha256:05f7c248e440f538aaad13eee78ef35f0541e73498dd6f832fe284542ac4b298"}, + {file = "grpcio-1.50.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:ca8a2254ab88482936ce941485c1c20cdeaef0efa71a61dbad171ab6758ec998"}, + {file = "grpcio-1.50.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3b611b3de3dfd2c47549ca01abfa9bbb95937eb0ea546ea1d762a335739887be"}, + {file = "grpcio-1.50.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a4cd8cb09d1bc70b3ea37802be484c5ae5a576108bad14728f2516279165dd7"}, + {file = "grpcio-1.50.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:156f8009e36780fab48c979c5605eda646065d4695deea4cfcbcfdd06627ddb6"}, + {file = "grpcio-1.50.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de411d2b030134b642c092e986d21aefb9d26a28bf5a18c47dd08ded411a3bc5"}, + {file = "grpcio-1.50.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d144ad10eeca4c1d1ce930faa105899f86f5d99cecfe0d7224f3c4c76265c15e"}, + {file = "grpcio-1.50.0-cp311-cp311-win32.whl", hash = "sha256:92d7635d1059d40d2ec29c8bf5ec58900120b3ce5150ef7414119430a4b2dd5c"}, + {file = "grpcio-1.50.0-cp311-cp311-win_amd64.whl", hash = "sha256:ce8513aee0af9c159319692bfbf488b718d1793d764798c3d5cff827a09e25ef"}, + {file = "grpcio-1.50.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:8e8999a097ad89b30d584c034929f7c0be280cd7851ac23e9067111167dcbf55"}, + {file = "grpcio-1.50.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:a50a1be449b9e238b9bd43d3857d40edf65df9416dea988929891d92a9f8a778"}, + {file = "grpcio-1.50.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:cf151f97f5f381163912e8952eb5b3afe89dec9ed723d1561d59cabf1e219a35"}, + {file = "grpcio-1.50.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a23d47f2fc7111869f0ff547f771733661ff2818562b04b9ed674fa208e261f4"}, + {file = "grpcio-1.50.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84d04dec64cc4ed726d07c5d17b73c343c8ddcd6b59c7199c801d6bbb9d9ed1"}, + {file = "grpcio-1.50.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:67dd41a31f6fc5c7db097a5c14a3fa588af54736ffc174af4411d34c4f306f68"}, + {file = "grpcio-1.50.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8d4c8e73bf20fb53fe5a7318e768b9734cf122fe671fcce75654b98ba12dfb75"}, + {file = "grpcio-1.50.0-cp37-cp37m-win32.whl", hash = "sha256:7489dbb901f4fdf7aec8d3753eadd40839c9085967737606d2c35b43074eea24"}, + {file = "grpcio-1.50.0-cp37-cp37m-win_amd64.whl", hash = "sha256:531f8b46f3d3db91d9ef285191825d108090856b3bc86a75b7c3930f16ce432f"}, + {file = "grpcio-1.50.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:d534d169673dd5e6e12fb57cc67664c2641361e1a0885545495e65a7b761b0f4"}, + {file = "grpcio-1.50.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:1d8d02dbb616c0a9260ce587eb751c9c7dc689bc39efa6a88cc4fa3e9c138a7b"}, + {file = "grpcio-1.50.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:baab51dcc4f2aecabf4ed1e2f57bceab240987c8b03533f1cef90890e6502067"}, + {file = "grpcio-1.50.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40838061e24f960b853d7bce85086c8e1b81c6342b1f4c47ff0edd44bbae2722"}, + {file = "grpcio-1.50.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:931e746d0f75b2a5cff0a1197d21827a3a2f400c06bace036762110f19d3d507"}, + {file = "grpcio-1.50.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:15f9e6d7f564e8f0776770e6ef32dac172c6f9960c478616c366862933fa08b4"}, + {file = "grpcio-1.50.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a4c23e54f58e016761b576976da6a34d876420b993f45f66a2bfb00363ecc1f9"}, + {file = "grpcio-1.50.0-cp38-cp38-win32.whl", hash = "sha256:3e4244c09cc1b65c286d709658c061f12c61c814be0b7030a2d9966ff02611e0"}, + {file = "grpcio-1.50.0-cp38-cp38-win_amd64.whl", hash = "sha256:8e69aa4e9b7f065f01d3fdcecbe0397895a772d99954bb82eefbb1682d274518"}, + {file = "grpcio-1.50.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:af98d49e56605a2912cf330b4627e5286243242706c3a9fa0bcec6e6f68646fc"}, + {file = "grpcio-1.50.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:080b66253f29e1646ac53ef288c12944b131a2829488ac3bac8f52abb4413c0d"}, + {file = "grpcio-1.50.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:ab5d0e3590f0a16cb88de4a3fa78d10eb66a84ca80901eb2c17c1d2c308c230f"}, + {file = "grpcio-1.50.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb11464f480e6103c59d558a3875bd84eed6723f0921290325ebe97262ae1347"}, + {file = "grpcio-1.50.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e07fe0d7ae395897981d16be61f0db9791f482f03fee7d1851fe20ddb4f69c03"}, + {file = "grpcio-1.50.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d75061367a69808ab2e84c960e9dce54749bcc1e44ad3f85deee3a6c75b4ede9"}, + {file = "grpcio-1.50.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ae23daa7eda93c1c49a9ecc316e027ceb99adbad750fbd3a56fa9e4a2ffd5ae0"}, + {file = "grpcio-1.50.0-cp39-cp39-win32.whl", hash = "sha256:177afaa7dba3ab5bfc211a71b90da1b887d441df33732e94e26860b3321434d9"}, + {file = "grpcio-1.50.0-cp39-cp39-win_amd64.whl", hash = "sha256:ea8ccf95e4c7e20419b7827aa5b6da6f02720270686ac63bd3493a651830235c"}, + {file = "grpcio-1.50.0.tar.gz", hash = "sha256:12b479839a5e753580b5e6053571de14006157f2ef9b71f38c56dc9b23b95ad6"}, ] importlib-metadata = [ {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"}, {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"}, ] -itsdangerous = [ - {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, - {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, -] -jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] -markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] -marshmallow = [ - {file = "marshmallow-3.18.0-py3-none-any.whl", hash = "sha256:35e02a3a06899c9119b785c12a22f4cda361745d66a71ab691fd7610202ae104"}, - {file = "marshmallow-3.18.0.tar.gz", hash = "sha256:6804c16114f7fce1f5b4dadc31f4674af23317fcc7f075da21e35c1a35d781f7"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] platformdirs = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] -pyjwt = [ - {file = "PyJWT-2.5.0-py3-none-any.whl", hash = "sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80"}, - {file = "PyJWT-2.5.0.tar.gz", hash = "sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b"}, +protobuf = [ + {file = "protobuf-4.21.9-cp310-abi3-win32.whl", hash = "sha256:6e0be9f09bf9b6cf497b27425487706fa48c6d1632ddd94dab1a5fe11a422392"}, + {file = "protobuf-4.21.9-cp310-abi3-win_amd64.whl", hash = "sha256:a7d0ea43949d45b836234f4ebb5ba0b22e7432d065394b532cdca8f98415e3cf"}, + {file = "protobuf-4.21.9-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5ab0b8918c136345ff045d4b3d5f719b505b7c8af45092d7f45e304f55e50a1"}, + {file = "protobuf-4.21.9-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:2c9c2ed7466ad565f18668aa4731c535511c5d9a40c6da39524bccf43e441719"}, + {file = "protobuf-4.21.9-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:e575c57dc8b5b2b2caa436c16d44ef6981f2235eb7179bfc847557886376d740"}, + {file = "protobuf-4.21.9-cp37-cp37m-win32.whl", hash = "sha256:9227c14010acd9ae7702d6467b4625b6fe853175a6b150e539b21d2b2f2b409c"}, + {file = "protobuf-4.21.9-cp37-cp37m-win_amd64.whl", hash = "sha256:a419cc95fca8694804709b8c4f2326266d29659b126a93befe210f5bbc772536"}, + {file = "protobuf-4.21.9-cp38-cp38-win32.whl", hash = "sha256:5b0834e61fb38f34ba8840d7dcb2e5a2f03de0c714e0293b3963b79db26de8ce"}, + {file = "protobuf-4.21.9-cp38-cp38-win_amd64.whl", hash = "sha256:84ea107016244dfc1eecae7684f7ce13c788b9a644cd3fca5b77871366556444"}, + {file = "protobuf-4.21.9-cp39-cp39-win32.whl", hash = "sha256:f9eae277dd240ae19bb06ff4e2346e771252b0e619421965504bd1b1bba7c5fa"}, + {file = "protobuf-4.21.9-cp39-cp39-win_amd64.whl", hash = "sha256:6e312e280fbe3c74ea9e080d9e6080b636798b5e3939242298b591064470b06b"}, + {file = "protobuf-4.21.9-py2.py3-none-any.whl", hash = "sha256:7eb8f2cc41a34e9c956c256e3ac766cf4e1a4c9c925dc757a41a01be3e852965"}, + {file = "protobuf-4.21.9-py3-none-any.whl", hash = "sha256:48e2cd6b88c6ed3d5877a3ea40df79d08374088e89bedc32557348848dff250b"}, + {file = "protobuf-4.21.9.tar.gz", hash = "sha256:61f21493d96d2a77f9ca84fefa105872550ab5ef71d21c458eb80edcf4885a99"}, ] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +pygments = [ + {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, + {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, +] +rich = [ + {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, + {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] typing-extensions = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] virtualenv = [ - {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, - {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, -] -werkzeug = [ - {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"}, - {file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"}, + {file = "virtualenv-20.16.6-py3-none-any.whl", hash = "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108"}, + {file = "virtualenv-20.16.6.tar.gz", hash = "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e"}, ] zipp = [ - {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, - {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, + {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, + {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, ] diff --git a/pyproject.toml b/pyproject.toml index 615ab1f..85f8c5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,15 +6,16 @@ description = "Managed isolated environments for Python" authors = ["Features & Labels "] [tool.poetry.dependencies] -python = ">=3.7" +python = ">=3.7,<4.0" virtualenv = ">=20.4" importlib-metadata = ">=4.4" -grpcio = { version = ">=1.49", optional = true } -protobuf = { version = "*", optional = true } +rich = ">=12.0" +grpcio = ">=1.49" +protobuf = "*" [tool.poetry.extras] -grpc = ["grpcio", "protobuf"] -server = ["grpcio", "protobuf"] +grpc = [] +server = [] [tool.poetry.plugins."isolate.backends"] "virtualenv" = "isolate.backends.virtualenv:VirtualPythonEnvironment" diff --git a/src/isolate/__init__.py b/src/isolate/__init__.py index c1ca3d6..42c6337 100644 --- a/src/isolate/__init__.py +++ b/src/isolate/__init__.py @@ -1,3 +1,8 @@ +from isolate.interface import ( + Box, + BoxedEnvironment, + LocalBox, + RemoteBox, + Template, +) from isolate.registry import prepare_environment - -__version__ = "0.1.0" diff --git a/src/isolate/backends/remote.py b/src/isolate/backends/remote.py index fc6b033..993df38 100644 --- a/src/isolate/backends/remote.py +++ b/src/isolate/backends/remote.py @@ -55,6 +55,9 @@ def create(self) -> EnvironmentDefinition: configuration=interface.to_struct(self.target_environment_config), ) + def exists(self) -> bool: + return False + def open_connection( self, connection_key: EnvironmentDefinition ) -> IsolateServerConnection: diff --git a/src/isolate/backends/settings.py b/src/isolate/backends/settings.py index ee27fee..cf74cd1 100644 --- a/src/isolate/backends/settings.py +++ b/src/isolate/backends/settings.py @@ -3,7 +3,7 @@ import shutil import tempfile from contextlib import contextmanager -from dataclasses import dataclass +from dataclasses import dataclass, replace from pathlib import Path from typing import TYPE_CHECKING, Callable, Iterator @@ -77,5 +77,7 @@ def cache_dir_for(self, backend: BaseEnvironment) -> Path: environment_base_path.mkdir(exist_ok=True, parents=True) return environment_base_path / backend.key + replace = replace + DEFAULT_SETTINGS = IsolateSettings() diff --git a/src/isolate/interface.py b/src/isolate/interface.py new file mode 100644 index 0000000..074c0b2 --- /dev/null +++ b/src/isolate/interface.py @@ -0,0 +1,306 @@ +from __future__ import annotations + +import importlib +import os +from concurrent.futures import ThreadPoolExecutor, as_completed +from contextlib import contextmanager +from dataclasses import dataclass, field, replace +from functools import lru_cache, partial +from typing import ( + Any, + Callable, + Dict, + Iterable, + Iterator, + List, + Optional, + Tuple, + TypeVar, + cast, +) + +import importlib_metadata +import rich +from rich.console import Console +from rich.status import Status +from rich.text import Text + +import isolate +from isolate.backends import BaseEnvironment +from isolate.backends.settings import IsolateSettings +from isolate.logs import Log, LogLevel, LogSource + +ReturnType = TypeVar("ReturnType") + +# Whether to enable debug logging or not. +_DEBUG_LOGGING = bool(os.environ.get("ISOLATE_ENABLE_DEBUG_LOGGING", False)) + +# List of serialization method options. Ordered by their +# priority/preference. +_SERIALIZATION_OPTIONS = ["dill", "cloudpickle"] + + +@lru_cache(1) +def _decide_default_backend(): + for option in _SERIALIZATION_OPTIONS: + try: + importlib.import_module(option) + except ImportError: + continue + else: + return option + + rich.print( + "Falling back to the default serialization method: 'pickle'.\n" + "For the best experience, please install one of the following: " + f"{', '.join(map(repr, _SERIALIZATION_OPTIONS))}" + ) + return "pickle" + + +def Template(kind: str, **config: Any) -> _EnvironmentBuilder: + """Create a new environment builder for the given kind (it can be virtualenv + or conda, depending on the flavor of packages you'd like to add). You can also + pass any configuration options that the backend supports.""" + + default_pkgs = [] + + default_backend = _decide_default_backend() + if default_backend != "pickle": + default_pkgs.append( + (default_backend, importlib_metadata.version(default_backend)) + ) + + if kind == "virtualenv": + requirements = config.setdefault("requirements", []) + requirements.extend(f"{name}=={version}" for name, version in default_pkgs) + return _PackageCollector(kind, config, collect_into=requirements) + elif kind == "conda": + packages = config.setdefault("packages", []) + packages.extend(f"{name}={version}" for name, version in default_pkgs) + return _PackageCollector(kind, config, collect_into=packages) + else: + raise NotImplementedError(f"Unknown environment kind: {kind}") + + +@dataclass +class _EnvironmentBuilder: + def get_definition(self) -> Tuple[Dict[str, Any], IsolateSettings]: + """Return the isolate definition for this environment!""" + raise NotImplementedError + + def __rshift__(self, left: Any) -> BoxedEnvironment: + """Put an environment into a box.""" + if not isinstance(left, Box): + return NotImplemented + + definition, settings = self.get_definition() + return left.wrap(definition, settings) + + def _not_supported(self, *args: Any, **kwargs: Any) -> None: + raise ValueError( + "Can't run a function on an environment template!" + "Be sure to forward it into a box first. Like: `environment = template >> box`" + ) + + run = _not_supported + map = _not_supported + + +@dataclass(repr=False) +class _PackageCollector(_EnvironmentBuilder): + kind: str + config_options: Dict[str, Any] + collect_into: List[str] + + def __lshift__(self, right: Any) -> Any: + if not isinstance(right, str): + return NotImplemented + + self.collect_into.append(right) + return self + + def get_definition(self) -> Tuple[Dict[str, Any], IsolateSettings]: + settings = IsolateSettings(serialization_method=_decide_default_backend()) + return { + "kind": self.kind, + **self.config_options, + }, settings + + def __repr__(self): + return f"{self.kind}({', '.join(f'{k}={v!r}' for k, v in self.config_options.items())})" + + +@dataclass +class Box: + """Some sort of a box/machine to run Python on.""" + + def wrap( + self, + definition: Dict[str, Any], + settings: IsolateSettings, + ) -> BoxedEnvironment: + raise NotImplementedError + + replace = replace + + +@dataclass +class LocalBox(Box): + """Run locally.""" + + pool_size: int = 1 + + def wrap( + self, + definition: Dict[str, Any], + settings: IsolateSettings, + ) -> BoxedEnvironment: + return BoxedEnvironment( + isolate.prepare_environment( + **definition, + context=settings, + ), + pool_size=self.pool_size, + ) + + def __mul__(self, right: int) -> LocalBox: + if not isinstance(right, int): + return NotImplemented + + return self.replace(pool_size=self.pool_size * right) + + +@dataclass +class RemoteBox(Box): + """Run on an hosted isolate server.""" + + host: str + pool_size: int = 1 + + def wrap( + self, + definition: Dict[str, Any], + settings: IsolateSettings, + ) -> BoxedEnvironment: + definition = definition.copy() + + # Extract the kind of environment to use. + kind = definition.pop("kind", None) + assert kind is not None, f"Corrupted definition: {definition}" + + # Create a remote environment. + return BoxedEnvironment( + isolate.prepare_environment( + "isolate-server", + host=self.host, + target_environment_kind=kind, + target_environment_config=definition, + context=settings, + ), + pool_size=self.pool_size, + ) + + def __mul__(self, right: int) -> RemoteBox: + if not isinstance(right, int): + return NotImplemented + + return self.replace(pool_size=self.pool_size * right) + + +@dataclass +class BoxedEnvironment: + """Environment-in-a-box! A user friendly wrapper around the isolate + environments!""" + + environment: BaseEnvironment + pool_size: int = 1 + _console: Console = field( + default_factory=partial(Console, highlighter=None), repr=False + ) + _status: Optional[Status] = field(default=None, repr=False) + _active_pool_size: Optional[str] = field(default=None, repr=False) + + def __post_init__(self): + existing_settings = self.environment.settings + new_settings = existing_settings.replace(log_hook=self._rich_log) + self.environment.apply_settings(new_settings) + + def _update_status(self, from_builder: bool = False) -> None: + if self._status is not None: + if from_builder: + self._status.update("Building the environment...", spinner="clock") + else: + if self._active_pool_size: + self._status.update( + f"Running the isolated tasks {self._active_pool_size}", + spinner="runner", + ) + else: + self._status.update("Running the isolated task", spinner="runner") + + def _rich_log(self, log: Log) -> None: + self._update_status(from_builder=log.source is LogSource.BUILDER) + if log.source is LogSource.USER: + # If the log is originating from user code, then print it + # as a normal message. + self._console.print(log.message) + else: + # Otherwise sprinkle some colors on it! + allowed_levels = { + LogLevel.ERROR: "red", + LogLevel.WARNING: "yellow", + LogLevel.INFO: "white", + } + if _DEBUG_LOGGING or log.source is LogSource.BUILDER: + allowed_levels[LogLevel.DEBUG] = "blue" + allowed_levels[LogLevel.TRACE] = "grey" + + if log.level in allowed_levels: + level = Text(f"[{log.source}]", style=allowed_levels[log.level]) + self._console.print(level + " " + log.message) + + @contextmanager + def _status_display(self, message: str) -> Iterator[None]: + assert self._status is None + + try: + self._status = self._console.status(message) + with self._status: + yield + finally: + self._status = None + self._active_pool_size = None + + def run( + self, + func: Callable[..., ReturnType], + *args: Any, + **kwargs: Any, + ) -> ReturnType: + """Run the given `func` in the environment with the passed arguments.""" + executable = partial(func, *args, **kwargs) + with self._status_display("Preparing for execution..."): + with self.environment.connect() as connection: + return cast(ReturnType, connection.run(executable)) + + def map( + self, + func: Callable[..., ReturnType], + *iterables: Iterable[Any], + ) -> Iterable[ReturnType]: + """Map the given `func` over the given iterables in parallel. pool_size + is determined by the originating box.""" + + with self._status_display("Preparing for execution..."): + with ThreadPoolExecutor(max_workers=self.pool_size) as executor: + with self.environment.connect() as connection: + futures = [ + executor.submit(connection.run, partial(func, *args)) + for args in zip(*iterables) + ] + self._active_pool_size = f"0/{len(futures)}" + for n, future in enumerate(as_completed(futures), 1): + yield cast(ReturnType, future.result()) + self._active_pool_size = f"{n}/{len(futures)}" + self._update_status() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7087f41 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,23 @@ +import pytest + +from isolate.backends.settings import IsolateSettings + + +@pytest.fixture +def isolate_server(monkeypatch, tmp_path): + from concurrent import futures + + import grpc + + from isolate.server import IsolateServicer, definitions + + monkeypatch.setattr("isolate.server.server.INHERIT_FROM_LOCAL", True) + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + test_settings = IsolateSettings(cache_dir=tmp_path / "cache") + definitions.register_isolate(IsolateServicer(test_settings), server) + host, port = "localhost", server.add_insecure_port(f"[::]:0") + server.start() + try: + yield f"{host}:{port}" + finally: + server.stop(None) diff --git a/tests/test_backends.py b/tests/test_backends.py index d734874..efa6bed 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -270,26 +270,6 @@ def test_local_python_environment(): local_env.destroy(connection_key) -@pytest.fixture -def isolate_server(monkeypatch, tmp_path): - from concurrent import futures - - import grpc - - from isolate.server import IsolateServicer, definitions - - monkeypatch.setattr("isolate.server.server.INHERIT_FROM_LOCAL", True) - server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) - test_settings = IsolateSettings(cache_dir=tmp_path / "cache") - definitions.register_isolate(IsolateServicer(test_settings), server) - host, port = "localhost", server.add_insecure_port(f"[::]:0") - server.start() - try: - yield f"{host}:{port}" - finally: - server.stop(None) - - def test_isolate_server_environment(isolate_server): environment = IsolateServer( host=isolate_server, diff --git a/tests/test_interactive.py b/tests/test_interactive.py new file mode 100644 index 0000000..c04eab5 --- /dev/null +++ b/tests/test_interactive.py @@ -0,0 +1,202 @@ +from dataclasses import dataclass +from typing import Any, Optional + +import importlib_metadata +import pytest + +from isolate.interface import BoxedEnvironment, LocalBox, RemoteBox, Template + +cp_version = importlib_metadata.version("cloudpickle") +dill_version = importlib_metadata.version("dill") + + +@pytest.mark.parametrize( + "kind, params, serialization_backend, expected", + [ + ( + "virtualenv", + {"requirements": []}, + "pickle", + "virtualenv(requirements=[])", + ), + ( + "conda", + {"packages": []}, + "cloudpickle", + f"conda(packages=['cloudpickle={cp_version}'])", + ), + ( + "virtualenv", + {"requirements": ["pandas"]}, + "pickle", + "virtualenv(requirements=['pandas'])", + ), + ( + "conda", + {"packages": ["pandas"]}, + "pickle", + "conda(packages=['pandas'])", + ), + ( + "virtualenv", + {"requirements": ["pyjokes==0.5.0"]}, + "cloudpickle", + f"virtualenv(requirements=['pyjokes==0.5.0', 'cloudpickle=={cp_version}'])", + ), + ( + "conda", + {"packages": ["pyjokes=0.5.0"]}, + "dill", + f"conda(packages=['pyjokes=0.5.0', 'dill={dill_version}'])", + ), + ], +) +def test_builder(kind, params, serialization_backend, expected, monkeypatch): + monkeypatch.setattr( + "isolate.interface._decide_default_backend", lambda: serialization_backend + ) + + builder = Template(kind, **params) + assert repr(builder) == expected + + +@pytest.mark.parametrize( + "kind, init_params, forwarded_packages, expected", + [ + ( + "virtualenv", + {"requirements": ["pandas"]}, + ["pyjokes"], + "virtualenv(requirements=['pandas', 'pyjokes'])", + ), + ( + "conda", + {"packages": ["pandas"]}, + ["pyjokes"], + "conda(packages=['pandas', 'pyjokes'])", + ), + ( + "virtualenv", + {"requirements": ["pyjokes==0.5.0"]}, + ["emoji==0.5.0", "pandas", "whatever==3.0.0"], + "virtualenv(requirements=['pyjokes==0.5.0', 'emoji==0.5.0', 'pandas', 'whatever==3.0.0'])", + ), + ( + "conda", + {"packages": ["pyjokes=0.5.0"]}, + ["emoji=0.5.0", "pandas", "whatever=3.0.0"], + "conda(packages=['pyjokes=0.5.0', 'emoji=0.5.0', 'pandas', 'whatever=3.0.0'])", + ), + ], +) +def test_builder_forwarding( + kind, init_params, forwarded_packages, expected, monkeypatch +): + # Use pickle to avoid adding the default backend to the requirements + monkeypatch.setattr("isolate.interface._decide_default_backend", lambda: "pickle") + + builder = Template(kind, **init_params) + for forwarded_package in forwarded_packages: + builder << forwarded_package + assert repr(builder) == expected + + +@dataclass +class UncachedLocalBox(LocalBox): + """Prevent caching of test environments when running + these tests locally.""" + + cache_dir: Optional[Any] = None + + def wrap(self, *args: Any, **kwargs: Any) -> BoxedEnvironment: + assert self.cache_dir is not None, "cache_dir must be set" + + boxed_env = super().wrap(*args, **kwargs) + boxed_env.environment.apply_settings( + boxed_env.environment.settings.replace(cache_dir=self.cache_dir) + ) + return boxed_env + + +def test_local_box(tmp_path): + builder = Template("virtualenv") + builder << "pyjokes==0.5.0" + + environment = builder >> UncachedLocalBox(cache_dir=tmp_path) + result = environment.run(eval, "__import__('pyjokes').__version__") + assert result == "0.5.0" + + +def test_remote_box(isolate_server): + builder = Template("virtualenv") + builder << "pyjokes==0.5.0" + + # Remote box is uncached by default (isolate_server handles it). + environment = builder >> RemoteBox(isolate_server) + result = environment.run(eval, "__import__('pyjokes').__version__") + assert result == "0.5.0" + + +def test_parallelism_local(tmp_path): + builder = Template("virtualenv") + environment = builder >> UncachedLocalBox(cache_dir=tmp_path) + + assert set(environment.map(eval, ["1", "2", "3", "4", "5", "6"])) == { + 1, + 2, + 3, + 4, + 5, + 6, + } + + +def test_parallelism_local_threads(tmp_path): + builder = Template("virtualenv") + environment = builder >> UncachedLocalBox(cache_dir=tmp_path) * 3 + + assert set(environment.map(eval, ["1", "2", "3", "4", "5", "6"])) == { + 1, + 2, + 3, + 4, + 5, + 6, + } + + +def test_parallelism_remote(isolate_server): + builder = Template("virtualenv") + environment = builder >> RemoteBox(isolate_server) + + assert set(environment.map(eval, ["1", "2", "3", "4", "5", "6"])) == { + 1, + 2, + 3, + 4, + 5, + 6, + } + + +def test_parallelism_remote_threads(isolate_server): + builder = Template("virtualenv") + environment = builder >> RemoteBox(isolate_server) * 3 + + assert set(environment.map(eval, ["1", "2", "3", "4", "5", "6"])) == { + 1, + 2, + 3, + 4, + 5, + 6, + } + + +def test_error_on_template_run(): + builder = Template("virtualenv") + with pytest.raises(ValueError): + builder.run(eval, "1") + + with pytest.raises(ValueError): + builder.map(eval, ["1", "2", "3"])