diff --git a/devchat/workflow/env_manager.py b/devchat/workflow/env_manager.py index a1178965..609de31f 100644 --- a/devchat/workflow/env_manager.py +++ b/devchat/workflow/env_manager.py @@ -4,6 +4,8 @@ import sys from typing import Dict, Optional, Tuple +import virtualenv + from devchat.utils import get_logger, get_logging_file from .envs import MAMBA_BIN_PATH @@ -66,7 +68,10 @@ def get_dep_hash(reqirements_file: str) -> str: return hashlib.md5(content.encode("utf-8")).hexdigest() def ensure( - self, env_name: str, py_version: str, reqirements_file: Optional[str] = None + self, + env_name: str, + py_version: Optional[str] = None, + reqirements_file: Optional[str] = None, ) -> Optional[str]: """ Ensure the python environment exists with the given name and version. @@ -82,7 +87,7 @@ def ensure( # check the version of the python executable current_version = self.get_py_version(py) - if current_version != py_version: + if py_version and current_version != py_version: should_remove_old = True if reqirements_file and self.should_reinstall(env_name, reqirements_file): @@ -99,8 +104,13 @@ def ensure( self.remove(env_name) # create the environment - print(f"- Creating {env_name} with {py_version}...", flush=True) - create_ok, msg = self.create(env_name, py_version) + if py_version: + print(f"- Creating {env_name} with {py_version}...", flush=True) + create_ok, msg = self.create(env_name, py_version) + else: + print(f"- Creating {env_name} with current Python version...", flush=True) + create_ok, msg = self.create_with_virtualenv(env_name) + if not create_ok: print(f"- Failed to create {env_name}.", flush=True) print(f"\nFor more details, check {log_file}.", flush=True) @@ -139,6 +149,21 @@ def ensure( print("\n```", flush=True) return self.get_py(env_name) + def create_with_virtualenv(self, env_name: str) -> Tuple[bool, str]: + """ + Create a new python environment using virtualenv with the current Python interpreter. + """ + env_path = os.path.join(MAMBA_PY_ENVS, env_name) + if os.path.exists(env_path): + return True, "" + + try: + # Use virtualenv.cli_run to create a virtual environment + virtualenv.cli_run([env_path, "--python", sys.executable]) + return True, "" + except Exception as e: + return False, str(e) + def install(self, env_name: str, requirements_file: str) -> Tuple[bool, str]: """ Install requirements into the python environment. @@ -256,7 +281,8 @@ def get_py(self, env_name: str) -> Optional[str]: env_path = None if sys.platform == "win32": env_path = os.path.join(MAMBA_PY_ENVS, env_name, "python.exe") - # env_path = os.path.join(MAMBA_PY_ENVS, env_name, "Scripts", "python.exe") + if not os.path.exists(env_path): + env_path = os.path.join(MAMBA_PY_ENVS, env_name, "Scripts", "python.exe") else: env_path = os.path.join(MAMBA_PY_ENVS, env_name, "bin", "python") diff --git a/devchat/workflow/schema.py b/devchat/workflow/schema.py index 5be1fc53..2015006e 100644 --- a/devchat/workflow/schema.py +++ b/devchat/workflow/schema.py @@ -5,7 +5,7 @@ class WorkflowPyConf(BaseModel): - version: str # python version + version: Optional[str] # python version dependencies: str # absolute path to the requirements file env_name: Optional[str] # python env name, will use the workflow name if not set diff --git a/poetry.lock b/poetry.lock index 380a6a79..9b5a6e93 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "anyio" @@ -157,6 +157,17 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + [[package]] name = "distro" version = "1.9.0" @@ -260,6 +271,22 @@ uvicorn = {version = ">=0.15.0", extras = ["standard"]} [package.extras] standard = ["uvicorn[standard] (>=0.15.0)"] +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] + [[package]] name = "gitdb" version = "4.0.11" @@ -697,6 +724,22 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + [[package]] name = "pluggy" version = "1.5.0" @@ -1338,6 +1381,26 @@ files = [ docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] +[[package]] +name = "virtualenv" +version = "20.27.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +files = [ + {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, + {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [[package]] name = "watchfiles" version = "0.24.0" @@ -1564,4 +1627,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "3256c8f1c52ea113266588adfd1db52cae7e894d38ed039b3f0155dd396d1d17" +content-hash = "57f7f378e9614d1638fc58e8547d36e0d5539c3da5b83eac7b7f837dd4e08fd6" diff --git a/pyproject.toml b/pyproject.toml index ccb0fa39..1ac4526d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ uvicorn = {extras = ["standard"], version = "^0.30.1"} gunicorn = "^22.0.0" loguru = "^0.7.2" win32-setctime = "^1.1.0" +virtualenv = "^20.27.1" [tool.poetry.scripts] devchat = "devchat._cli.main:main"