diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..c444918 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 88 +extend-ignore = E501 +exclude = tests/* src/* \ No newline at end of file diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 4a9aeca..1abd2e7 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -1,87 +1,118 @@ # Name of the GitHub Actions workflow -name: Python Virtual Environment Setup CI +name: Python Virtual Environment Creator Tests -# Define when this workflow should run +# Define when this workflow should be triggered on: + # Trigger on push events to main and develop branches push: - branches: [ main ] # Trigger on pushes to main branch + branches: [ main, develop ] + # Trigger on pull requests to main and develop branches pull_request: - branches: [ main ] # Trigger on pull requests to main branch + branches: [ main, develop ] -# Define the jobs to run +# Define the jobs to run as part of this workflow jobs: - # First job: testing across different OS and Python versions + # Job for running tests across different environments test: - # Dynamic OS selection based on matrix strategy + # Dynamic name showing OS and Python version being tested + name: Test on ${{ matrix.os }} / Python ${{ matrix.python-version }} + # OS to run the job on, pulled from matrix strategy runs-on: ${{ matrix.os }} strategy: + # Continue running other matrix combinations even if one fails + fail-fast: false + # Define test matrix - will run tests on all combinations of these matrix: - # Define test matrix: will run tests on all combinations of these - os: [ubuntu-latest, windows-latest, macos-latest] # Test on all major OS - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] # Test on multiple Python versions + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - # Step 1: Check out the repository code + # Check out the repository code - uses: actions/checkout@v3 - - # Step 2: Set up Python environment + + # Set up Python environment with specified version - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - # Step 3: Install required Python packages + architecture: x64 + + # Install required Python packages for testing - name: Install dependencies run: | - python -m pip install --upgrade pip # Upgrade pip to latest version - pip install pytest pytest-cov flake8 # Install testing and linting tools - - # Step 4: Run code quality checks with flake8 - - name: Lint with flake8 - run: | - # Check for specific critical errors - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # Check overall code quality - flake8 . --count --max-complexity=10 --max-line-length=127 --statistics - - # Step 5: Create a test virtual environment - - name: Create test virtual environment + python -m pip install --upgrade pip + pip install -r requirements.txt + + # Run static type checking with mypy in strict mode + - name: Run type checking run: | - python -m venv test_venv - - # Step 6: Run tests with coverage reporting - - name: Test VenvCreator + mypy src tests + + # Run tests with pytest and generate coverage report + - name: Run tests with coverage run: | - pytest --cov=. --cov-report=xml # Run tests and generate coverage report - - # Step 7: Upload coverage reports to Codecov + pytest tests -v --cov=src. --cov-report=xml + + # Upload test coverage data to Codecov - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: - file: ./coverage.xml # Coverage report file - flags: unittests # Tag these results as unit tests - fail_ci_if_error: true # Fail if upload to Codecov fails + file: ./coverage.xml + flags: unittests + name: codecov-${{ matrix.os }}-py${{ matrix.python-version }} + fail_ci_if_error: false + + # Job for code linting checks + lint: + name: Lint + runs-on: ubuntu-latest + steps: + # Check out repository code + - uses: actions/checkout@v3 + + # Set up Python 3.11 environment for linting + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + # Install linting tools + - name: Install dependencies + run: | + python -m pip install -r requirements.txt - # Second job: security scanning + # Run various linting checks: + # - flake8 for code style and errors + # - black for code formatting + # - isort for import sorting + - name: Run linters # noqa: E501 + run: | + flake8 src tests + black src tests --check + isort src tests --check-only --profile black # noqa: E501 + + # Job for security vulnerability scanning security: - runs-on: ubuntu-latest # Security checks only need to run on one OS + name: Security checks + runs-on: ubuntu-latest steps: - # Step 1: Check out the repository code + # Check out repository code - uses: actions/checkout@v3 - - # Step 2: Set up Python environment + + # Set up Python 3.11 environment for security checks - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' # Use Python 3.10 for security checks - - # Step 3: Install security scanning tools - - name: Install security scanning tools + python-version: '3.11' + + # Install security scanning tools + - name: Install dependencies run: | - pip install bandit safety # bandit for code scanning, safety for dependency checking - - # Step 4: Run security scans - - name: Run security scan + python -m pip install -r requirements.txt + + # Run security checks: + # - bandit for code security issues + # - safety for known vulnerabilities in dependencies + - name: Run security checks run: | - bandit -r . # Recursively scan all Python files for security issues - safety check # Check dependencies for known security vulnerabilities \ No newline at end of file + bandit -r src tests \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efa407c --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/create_venv.py b/create_venv.py deleted file mode 100644 index bc9981e..0000000 --- a/create_venv.py +++ /dev/null @@ -1,371 +0,0 @@ -""" -I WANT TO ADD ALL OF THESE BUT I KNOW THIS IS A HUGE LEAP -Add logging functionality for better debugging -Include a way to specify Python version requirements -Add support for pip.conf/pip.ini configuration -Include option to upgrade pip in new environments -Add support for conda environments -""" -import os -import subprocess -import glob -import sys -import platform -import shutil -from pathlib import Path - - -class VenvCreator: - def __init__(self): - self.os_type = platform.system().lower() - self.is_windows = self.os_type == 'windows' - self.is_linux = self.os_type in ['linux', 'darwin'] # darwin is macOS - self.python_installations = [] - self.username = self.get_current_username() - self.dependencies_folder = None - - def set_dependencies_folder(self, folder_path): - """Set the dependencies folder path""" - if os.path.exists(folder_path): - self.dependencies_folder = folder_path - return True - return False - - def find_dependencies(self): - """Find all dependency files in the dependencies folder""" - if not self.dependencies_folder: - return [] - - dependency_files = [] - for ext in ['.whl', '.tar.gz', '.zip']: - dependency_files.extend(glob.glob(os.path.join(self.dependencies_folder, f'*{ext}'))) - return dependency_files - - def install_local_dependencies(self, venv_path): - """Install dependencies from local folder""" - if not self.dependencies_folder: - return None - - pip_path = os.path.join(venv_path, 'Scripts' if self.is_windows else 'bin', 'pip' + ('.exe' if self.is_windows else '')) - dependencies = self.find_dependencies() - - if not dependencies: - print("No dependency files found in the specified folder.") - return False - - try: - print("\nInstalling local dependencies...") - for dep in dependencies: - print(f"Installing {os.path.basename(dep)}") - subprocess.run([pip_path, 'install', dep], check=True) - return True - except subprocess.CalledProcessError as e: - print(f"Error installing local dependencies: {e}") - return False - - def get_current_username(self): - """Get current username based on OS""" - if self.is_windows: - return os.getenv('USERNAME') - return os.getenv('USER') - - def find_python_installations(self): - """Find Python installations based on OS""" - if self.is_windows: - return self._find_windows_python() - return self._find_linux_python() - - def _find_windows_python(self): - """Find Python installations on Windows""" - installations_with_versions = [] - - # Search in AppData location - appdata_path = f"C:\\Users\\{self.username}\\AppData\\Local\\Programs\\Python\\Python*\\python.exe" - program_files_paths = [ - "C:\\Program Files\\Python*\\python.exe", - "C:\\Program Files (x86)\\Python*\\python.exe" - ] - - # Collect all potential paths - all_paths = [] - all_paths.extend(glob.glob(appdata_path)) - for path in program_files_paths: - all_paths.extend(glob.glob(path)) - - # Create list with version information for sorting - for path in all_paths: - version = self.get_python_version(path) - try: - # Extract version numbers from string like "Python 3.10.11" - version_numbers = version.replace("Python ", "").split(".") - version_tuple = tuple(int(num) for num in version_numbers) - installations_with_versions.append((path, version, version_tuple)) - except Exception: - continue - - # Sort by version tuple - sorted_installations = sorted(installations_with_versions, key=lambda x: x[2]) - # Return only the paths in sorted order - return [installation[0] for installation in sorted_installations] - - def _find_linux_python(self): - """Find Python installations on Linux/Unix""" - python_installations = [] - - # Common Linux Python locations - linux_paths = [ - "/usr/bin/python3*", - "/usr/local/bin/python3*", - f"/home/{self.username}/.local/bin/python3*" - ] - - for path in linux_paths: - found_paths = glob.glob(path) - # Filter out symbolic links and keep only actual executables - for p in found_paths: - if os.path.isfile(p) and not os.path.islink(p): - python_installations.append(p) - - return sorted(set(python_installations)) # Remove duplicates - - def get_python_version(self, python_path): - """Get Python version for a given Python executable""" - try: - result = subprocess.run([python_path, '--version'], capture_output=True, text=True) - return result.stdout.strip() - except Exception: - return "Version unknown" - - def create_venv(self, python_path, venv_path): - """Create virtual environment using specified Python installation""" - try: - subprocess.run([python_path, '-m', 'venv', venv_path], check=True) - return True - except subprocess.CalledProcessError as e: - print(f"Error creating virtual environment: {e}") - return False - - def install_requirements(self, venv_path, requirements_path=None): - """Install requirements if requirements.txt exists""" - # This version: - # Checks if each dependency file exists before attempting installation - # Maintains a list of missing files - # Verifies that existing files are not empty - # Provides detailed error messages showing: - # Which files are missing - # The expected location of missing files - # Only proceeds with installation if all required files are present - # Handles both general exceptions and specific pip installation errors - # Provides more detailed feedback about the installation process - - if not requirements_path or not os.path.exists(requirements_path): - return None - - # Get the project root directory (where requirements.txt is located) - project_root = os.path.dirname(requirements_path) - - # Create a temporary requirements file with absolute paths - temp_requirements = os.path.join(project_root, 'temp_requirements.txt') - - try: - missing_files = [] - with open(requirements_path, 'r') as original: - with open(temp_requirements, 'w') as temp: - for line in original: - line = line.strip() - if line.startswith('dependencies/'): - # Convert relative path to absolute path - relative_path = line - absolute_path = os.path.abspath(os.path.join(project_root, line)) - - # Check if file exists - if not os.path.exists(absolute_path): - missing_files.append(relative_path) - print(f"Warning: Required file not found: {relative_path}") - print(f"Expected location: {absolute_path}") - else: - # Verify if it's a valid file - if os.path.getsize(absolute_path) == 0: - print(f"Warning: File exists but appears to be empty: {relative_path}") - temp.write(f"{absolute_path}\n") - else: - temp.write(f"{line}\n") - - if missing_files: - print("\nMissing dependency files:") - for file in missing_files: - print(f"- {file}") - return False - - pip_path = os.path.join(venv_path, 'Scripts' if self.is_windows else 'bin', 'pip' + ('.exe' if self.is_windows else '')) - - print("\nAll dependency files found. Installing requirements...") - subprocess.run([pip_path, 'install', '-r', temp_requirements], check=True) - return True - - except subprocess.CalledProcessError as e: - print(f"Error installing requirements: {e}") - return False - except Exception as e: - print(f"Unexpected error during requirements installation: {e}") - return False - finally: - # Clean up temporary file - if os.path.exists(temp_requirements): - os.remove(temp_requirements) - - def get_project_details(self): - """Get project details from user""" - project_details = {} - - # Get project name - project_details['name'] = input("\nEnter project name: ").strip() - - # Get project path - while True: - project_path = input("Enter full project path: ").strip().strip('"') - if os.path.exists(project_path): - project_details['path'] = project_path - break - create_dir = input("Directory doesn't exist. Create it? (y/n): ").lower() - if create_dir == 'y': - try: - os.makedirs(project_path) - project_details['path'] = project_path - break - except Exception as e: - print(f"Error creating directory: {e}") - - # Get virtual environment name - while True: - venv_name = input("Enter virtual environment name (e.g., venv, .venv, project-env): ").strip() - if venv_name: - project_details['venv_name'] = venv_name - break - print("Virtual environment name cannot be empty.") - - # Ask about dependencies folder - use_deps = input("Do you want to install dependencies from a local folder? (y/n): ").lower() - if use_deps == 'y': - while True: - deps_path = input("Enter dependencies folder path: ").strip().strip('"') - if self.set_dependencies_folder(deps_path): - project_details['dependencies_path'] = deps_path - break - print("Invalid dependencies folder path. Please try again.") - - # Handle requirements.txt - requirements_path = os.path.join(project_path, 'requirements.txt') - if os.path.exists(requirements_path): - project_details['requirements_path'] = requirements_path - print(f"Found requirements.txt at: {requirements_path}") - else: - create_req = input("No requirements.txt found. Create one? (y/n): ").lower() - if create_req == 'y': - project_details['create_requirements'] = True - print("\nEnter package names (one per line, press Enter twice when done):") - packages = [] - while True: - package = input().strip() - if not package: - break - packages.append(package) - if packages: - with open(requirements_path, 'w') as f: - f.write('\n'.join(packages)) - project_details['requirements_path'] = requirements_path - - return project_details - - def print_activation_instructions(self, project_path, venv_name): - """Print OS-specific activation instructions""" - print("\nTo activate the virtual environment:") - - if self.is_windows: - print(f"cd {project_path}") - print(f"{venv_name}\\Scripts\\activate") - else: - print(f"cd {project_path}") - print(f"source {venv_name}/bin/activate") - - print("\nAdditional commands:") - print("pip list - Show installed packages") - print("pip freeze > requirements.txt - Update requirements file") - print("deactivate - Exit virtual environment") - - def run(self): - """Main execution method""" - print(f"\n=== Python Virtual Environment Setup ({self.os_type.capitalize()}) ===") - - # Find Python installations - installations = self.find_python_installations() - if not installations: - print("No Python installations found!") - return - - # Display found Python installations in sorted order - print("\nFound Python installations:") - for i, path in enumerate(installations, 1): - version = self.get_python_version(path) - print(f"{i}. {path} - {version}") - - # Get user selection for Python version - while True: - try: - choice = int(input("\nSelect Python installation (enter number): ")) - 1 - if 0 <= choice < len(installations): - selected_python = installations[choice] - break - print("Invalid selection. Please try again.") - except ValueError: - print("Please enter a valid number.") - - # Get project details - project_details = self.get_project_details() - - # Create virtual environment path - venv_path = os.path.join(project_details['path'], project_details['venv_name']) - - # Check if venv already exists - if os.path.exists(venv_path): - overwrite = input(f"\nVirtual environment already exists at {venv_path}. Overwrite? (y/n): ").lower() - if overwrite == 'y': - shutil.rmtree(venv_path) - else: - print("Operation cancelled.") - return - - # Create virtual environment - print(f"\nCreating virtual environment using {selected_python}") - print(f"Location: {venv_path}") - - if self.create_venv(selected_python, venv_path): - print("\nVirtual environment created successfully!") - - # Install local dependencies if specified - if self.dependencies_folder: - result = self.install_local_dependencies(venv_path) - if result: - print("Local dependencies installed successfully!") - elif result is False: - print("Failed to install local dependencies.") - - # Install requirements if they exist - if 'requirements_path' in project_details: - result = self.install_requirements(venv_path, project_details['requirements_path']) - if result: - print("Requirements installed successfully!") - elif result is False: - print("Failed to install requirements.") - - # Print activation instructions - self.print_activation_instructions(project_details['path'], project_details['venv_name']) - else: - print("\nFailed to create virtual environment.") - -def main(): - venv_creator = VenvCreator() - venv_creator.run() - -if __name__ == "__main__": - main() diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..bf78a9d --- /dev/null +++ b/mypy.ini @@ -0,0 +1,19 @@ +[mypy] +python_version = 3.8 +warn_return_any = True +warn_unused_configs = True +disallow_untyped_defs = False +disallow_incomplete_defs = False +check_untyped_defs = True +disallow_untyped_decorators = False +no_implicit_optional = False +warn_redundant_casts = True +warn_unused_ignores = True +warn_no_return = True +warn_unreachable = True + +[mypy-pytest.*] +ignore_missing_imports = True + +[mypy-setuptools.*] +ignore_missing_imports = True \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..77fdb33 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +# Testing dependencies +pytest +pytest-cov +mypy +types-setuptools + +# Linting tools +flake8 +black +isort + +# Security tools +bandit \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d09e076 --- /dev/null +++ b/setup.py @@ -0,0 +1,8 @@ +from setuptools import setup, find_packages + +setup( + name="venv_creator", + version="0.1", + packages=find_packages(), + install_requires=[], +) \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/venv_creator.py b/src/venv_creator.py new file mode 100644 index 0000000..1f6d4fd --- /dev/null +++ b/src/venv_creator.py @@ -0,0 +1,361 @@ +""" +_summary_ +""" +import os +import subprocess # nosec B404 +import glob +import platform +import shutil +from typing import List, Dict, Optional, Tuple + + +class VenvCreator: + """ + _summary_ + """ + def __init__(self): + self.os_type = platform.system().lower() + self.is_windows = self.os_type == 'windows' + self.is_linux = self.os_type in ['linux', 'darwin'] + self.python_installations: List[str] = [] + self.username = self.get_current_username() + self.dependencies_folder: Optional[str] = None + + def set_dependencies_folder(self, folder_path: str) -> bool: + """Set the dependencies folder path""" + folder_path = os.path.expanduser(folder_path) + if os.path.exists(folder_path): + self.dependencies_folder = folder_path + return True + return False + + def find_dependencies(self) -> List[str]: + """Find all dependency files in the dependencies folder""" + if not self.dependencies_folder: + return [] + + dependency_files = [] + extensions = ['.whl', '.tar.gz', '.zip'] + for ext in extensions: + dependency_files.extend(glob.glob( + os.path.join(self.dependencies_folder, f'*{ext}'))) + return sorted(dependency_files) + + def install_local_dependencies(self, venv_path: str) -> Optional[bool]: + """Install dependencies from local folder""" + if not self.dependencies_folder: + return None + + pip_path = self._get_pip_path(venv_path) + dependencies = self.find_dependencies() + + if not dependencies: + print("No dependency files found in the specified folder.") + return False + + try: + print("\nInstalling local dependencies...") + for dep in dependencies: + dep_name = os.path.basename(dep) + print(f"Installing {dep_name}") + result = subprocess.run( # nosec B603 + [pip_path, 'install', dep], + check=True, + capture_output=True, + text=True + ) + if result.stderr: + print(f"Warning during installation: {result.stderr}") + return True + except subprocess.CalledProcessError as e: + print(f"Error installing local dependencies: {e}") + if e.stderr: + print(f"Error details: {e.stderr}") + return False + + def get_current_username(self) -> Optional[str]: + """Get current username based on OS""" + return os.getenv('USERNAME' if self.is_windows else 'USER') + + def _get_pip_path(self, venv_path: str) -> str: + """Get the pip executable path for the virtual environment""" + return os.path.join( + venv_path, + 'Scripts' if self.is_windows else 'bin', + 'pip' + ('.exe' if self.is_windows else '') + ) + + def find_python_installations(self) -> List[str]: + """Find Python installations based on OS""" + return self._find_windows_python( + ) if self.is_windows else self._find_linux_python() + + def _find_windows_python(self) -> List[str]: + """Find Python installations on Windows""" + installations_with_versions: List[ + Tuple[str, str, Tuple[int, ...]]] = [] + search_paths = [ + f"C:\\Users\\{self.username}\\AppData\\Local\\Programs\\Python\\Python*\\python.exe", + "C:\\Program Files\\Python*\\python.exe", + "C:\\Program Files (x86)\\Python*\\python.exe" + ] + + for search_path in search_paths: + for path in glob.glob(search_path): + version = self.get_python_version(path) + try: + version_numbers = version.replace("Python ", "").split(".") + version_tuple = tuple(int(num) for num in version_numbers) + installations_with_versions.append((path, version, version_tuple)) + except (ValueError, AttributeError): + continue + + return [installation[0] for installation in + sorted(installations_with_versions, key=lambda x: x[2])] + + def _find_linux_python(self) -> List[str]: + """Find Python installations on Linux/Unix""" + search_paths = [ + "/usr/bin/python3*", + "/usr/local/bin/python3*", + f"/home/{self.username}/.local/bin/python3*" + ] + + python_installations = set() + for path_pattern in search_paths: + for path in glob.glob(path_pattern): + if os.path.isfile(path) and not os.path.islink(path): + python_installations.add(path) + + return sorted(python_installations) + + def get_python_version(self, python_path: str) -> str: + """Get Python version for a given Python executable""" + try: + result = subprocess.run( # nosec B603 + [python_path, '--version'], + capture_output=True, + text=True, + timeout=5, + check=False + ) + return result.stdout.strip() + except (subprocess.SubprocessError, OSError): + return "Version unknown" + + def create_venv(self, python_path: str, venv_path: str) -> bool: + """Create virtual environment using specified Python installation""" + try: + result = subprocess.run( # nosec B603 + [python_path, '-m', 'venv', venv_path], + check=True, + capture_output=True, + text=True + ) + if result.stderr: + print(f"Warning during venv creation: {result.stderr}") + return True + except subprocess.CalledProcessError as e: + print(f"Error creating virtual environment: {e}") + if e.stderr: + print(f"Error details: {e.stderr}") + return False + + def install_requirements(self, venv_path: str, requirements_path: Optional[str] = None) -> Optional[bool]: + """Install requirements if requirements.txt exists""" + if not requirements_path or not os.path.exists(requirements_path): + return None + + project_root = os.path.dirname(requirements_path) + temp_requirements = os.path.join(project_root, 'temp_requirements.txt') + + try: + missing_files = [] + with open(requirements_path, mode='r', encoding='utf-8') as original: + with open(temp_requirements, mode='w', encoding='utf-8') as temp: + for line in original: + line = line.strip() + if line.startswith('dependencies/'): + absolute_path = os.path.abspath(os.path.join(project_root, line)) + if not os.path.exists(absolute_path): + missing_files.append(line) + print(f"Warning: Required file not found: {line}") + print(f"Expected location: {absolute_path}") + elif os.path.getsize(absolute_path) == 0: + print(f"Warning: File exists but is empty: {line}") + missing_files.append(line) + else: + temp.write(f"{absolute_path}\n") + else: + temp.write(f"{line}\n") + + if missing_files: + print("\nMissing or invalid dependency files:") + for file in missing_files: + print(f"- {file}") + return False + + pip_path = self._get_pip_path(venv_path) + print("\nInstalling requirements...") + result = subprocess.run( # nosec B603 + [pip_path, 'install', '-r', temp_requirements], + check=True, + capture_output=True, + text=True + ) + if result.stderr: + print(f"Warnings during installation: {result.stderr}") + return True + + except subprocess.CalledProcessError as e: + print(f"Error installing requirements: {e}") + if e.stderr: + print(f"Error details: {e.stderr}") + return False + except Exception as e: + print(f"Unexpected error during requirements installation: {e}") + return False + finally: + if os.path.exists(temp_requirements): + os.remove(temp_requirements) + + def get_project_details(self) -> Dict[str, str]: + """Get project details from user""" + project_details: Dict[str, str] = {} + + project_details['name'] = input("\nEnter project name: ").strip() + + while True: + project_path = os.path.expanduser(input("Enter full project path: ").strip().strip('"')) + if os.path.exists(project_path): + project_details['path'] = project_path + break + create_dir = input("Directory doesn't exist. Create it? (y/n): ").lower() + if create_dir == 'y': + try: + os.makedirs(project_path) + project_details['path'] = project_path + break + except OSError as e: + print(f"Error creating directory: {e}") + + while True: + venv_name = input("Enter virtual environment name (e.g., venv, .venv): ").strip() + if venv_name: + project_details['venv_name'] = venv_name + break + print("Virtual environment name cannot be empty.") + + if input("Do you want to install dependencies from a local folder? (y/n): ").lower() == 'y': + while True: + deps_path = os.path.expanduser(input("Enter dependencies folder path: ").strip().strip('"')) + if self.set_dependencies_folder(deps_path): + project_details['dependencies_path'] = deps_path + break + print("Invalid dependencies folder path. Please try again.") + + requirements_path = os.path.join(project_path, 'requirements.txt') + if os.path.exists(requirements_path): + project_details['requirements_path'] = requirements_path + print(f"Found requirements.txt at: {requirements_path}") + elif input("No requirements.txt found. Create one? (y/n): ").lower() == 'y': + print("\nEnter package names (one per line, press Enter twice when done):") + packages = [] + while True: + package = input().strip() + if not package: + break + packages.append(package) + if packages: + with open(requirements_path, mode='w', encoding='utf-8') as f: + f.write('\n'.join(packages)) + project_details['requirements_path'] = requirements_path + + return project_details + + def print_activation_instructions(self, project_path: str, venv_name: str) -> None: + """Print OS-specific activation instructions""" + print("\nTo activate the virtual environment:") + + activation_cmd = f"source {venv_name}/bin/activate" + if self.is_windows: + activation_cmd = f"{venv_name}\\Scripts\\activate" + + print(f"cd {project_path}") + print(activation_cmd) + + print("\nUseful commands:") + print("pip list - Show installed packages") + print("pip freeze - List installed packages in requirements format") + print("pip check - Verify dependencies have compatible versions") + print("deactivate - Exit virtual environment") + + def run(self) -> None: + """Main execution method""" + print(f"\n=== Python Virtual Environment Setup ({self.os_type.capitalize()}) ===") + + installations = self.find_python_installations() + if not installations: + print("No Python installations found!") + return + + print("\nFound Python installations:") + for i, path in enumerate(installations, 1): + version = self.get_python_version(path) + print(f"{i}. {path} - {version}") + + while True: + try: + choice = int(input("\nSelect Python installation (enter number): ")) - 1 + if 0 <= choice < len(installations): + selected_python = installations[choice] + break + print("Invalid selection. Please try again.") + except ValueError: + print("Please enter a valid number.") + + project_details = self.get_project_details() + venv_path = os.path.join(project_details['path'], project_details['venv_name']) + + if os.path.exists(venv_path): + if input(f"\nVirtual environment already exists at {venv_path}. Overwrite? (y/n): ").lower() == 'y': + shutil.rmtree(venv_path) + else: + print("Operation cancelled.") + return + + print(f"\nCreating virtual environment using {selected_python}") + print(f"Location: {venv_path}") + + if self.create_venv(selected_python, venv_path): + print("\nVirtual environment created successfully!") + + if self.dependencies_folder: + result = self.install_local_dependencies(venv_path) + if result: + print("Local dependencies installed successfully!") + elif result is False: + print("Failed to install local dependencies.") + + if 'requirements_path' in project_details: + result = self.install_requirements(venv_path, project_details['requirements_path']) + if result: + print("Requirements installed successfully!") + elif result is False: + print("Failed to install requirements.") + + self.print_activation_instructions(project_details['path'], project_details['venv_name']) + else: + print("\nFailed to create virtual environment.") + + +def main(): + """ + _summary_ + """ + venv_creator = VenvCreator() + venv_creator.run() + + +if __name__ == "__main__": + main() diff --git a/test_venv_creator.py b/test_venv_creator.py deleted file mode 100644 index 90d0fc8..0000000 --- a/test_venv_creator.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest -import os -from create_venv import VenvCreator - -@pytest.fixture -def venv_creator(): - return VenvCreator() - -def test_init(venv_creator): - assert hasattr(venv_creator, 'os_type') - assert hasattr(venv_creator, 'is_windows') - assert hasattr(venv_creator, 'is_linux') - -def test_find_python_installations(venv_creator): - installations = venv_creator.find_python_installations() - assert isinstance(installations, list) - assert len(installations) > 0 - -def test_get_python_version(venv_creator): - python_path = "python" # assumes python is in PATH - version = venv_creator.get_python_version(python_path) - assert isinstance(version, str) - assert "Python" in version - -def test_set_dependencies_folder(venv_creator, tmp_path): - # Test with valid path - assert venv_creator.set_dependencies_folder(str(tmp_path)) == True - - # Test with invalid path - assert venv_creator.set_dependencies_folder("/nonexistent/path") == False \ No newline at end of file diff --git a/tests/test_venv_creator.py b/tests/test_venv_creator.py new file mode 100644 index 0000000..be0c649 --- /dev/null +++ b/tests/test_venv_creator.py @@ -0,0 +1,157 @@ +""" +_summary_ +""" +import os +import sys +import platform +import subprocess +import tempfile +import shutil +import unittest +from unittest.mock import patch, MagicMock +from pathlib import Path +from typing import List, Dict + +# Import the VenvCreator class +from venv_creator import VenvCreator + +class TestVenvCreator(unittest.TestCase): + """ + _summary_ + """ + def setUp(self): + """Set up test environment before each test""" + self.venv_creator = VenvCreator() + self.temp_dir = tempfile.mkdtemp() + self.test_requirements = os.path.join(self.temp_dir, 'requirements.txt') + self.test_dependencies = os.path.join(self.temp_dir, 'dependencies') + os.makedirs(self.test_dependencies) + + def tearDown(self): + """Clean up test environment after each test""" + shutil.rmtree(self.temp_dir) + + def create_test_wheel(self, name: str = "test_package-1.0-py3-none-any.whl"): + """Helper method to create a dummy wheel file""" + wheel_path = os.path.join(self.test_dependencies, name) + with open(wheel_path, 'w') as f: + f.write('dummy wheel file') + return wheel_path + + def test_init(self): + """Test initialization of VenvCreator""" + self.assertEqual(self.venv_creator.os_type, platform.system().lower()) + self.assertEqual(self.venv_creator.is_windows, platform.system().lower() == 'windows') + self.assertEqual(self.venv_creator.is_linux, platform.system().lower() in ['linux', 'darwin']) + + def test_set_dependencies_folder(self): + """Test setting dependencies folder""" + # Test with valid path + self.assertTrue(self.venv_creator.set_dependencies_folder(self.test_dependencies)) + self.assertEqual(self.venv_creator.dependencies_folder, self.test_dependencies) + + # Test with invalid path + invalid_path = os.path.join(self.temp_dir, 'nonexistent') + self.assertFalse(self.venv_creator.set_dependencies_folder(invalid_path)) + + def test_find_dependencies(self): + """Test finding dependency files""" + # Create test wheel files + wheel1 = self.create_test_wheel("package1-1.0-py3-none-any.whl") + wheel2 = self.create_test_wheel("package2-1.0-py3-none-any.whl") + targz = os.path.join(self.test_dependencies, "package3-1.0.tar.gz") + with open(targz, 'w') as f: + f.write('dummy targz file') + + self.venv_creator.set_dependencies_folder(self.test_dependencies) + dependencies = self.venv_creator.find_dependencies() + + self.assertEqual(len(dependencies), 3) + self.assertIn(wheel1, dependencies) + self.assertIn(wheel2, dependencies) + self.assertIn(targz, dependencies) + + @patch('subprocess.run') + def test_install_local_dependencies(self, mock_run): + """Test installing local dependencies""" + mock_run.return_value = MagicMock(stderr="", returncode=0) + self.create_test_wheel() + + venv_path = os.path.join(self.temp_dir, 'venv') + os.makedirs(venv_path) + + self.venv_creator.set_dependencies_folder(self.test_dependencies) + result = self.venv_creator.install_local_dependencies(venv_path) + + self.assertTrue(result) + mock_run.assert_called() + + def test_get_python_version(self): + """Test getting Python version""" + current_python = sys.executable + version = self.venv_creator.get_python_version(current_python) + self.assertIn('Python', version) + + @patch('subprocess.run') + def test_create_venv(self, mock_run): + """Test creating virtual environment""" + mock_run.return_value = MagicMock(stderr="", returncode=0) + + venv_path = os.path.join(self.temp_dir, 'venv') + result = self.venv_creator.create_venv(sys.executable, venv_path) + + self.assertTrue(result) + mock_run.assert_called_once() + + def test_get_project_details(self): + """Test getting project details with mocked input""" + test_inputs = [ + 'test_project', # project name + self.temp_dir, # project path + 'venv', # venv name + 'n', # install dependencies? + 'n' # create requirements.txt? + ] + + with patch('builtins.input', side_effect=test_inputs): + details = self.venv_creator.get_project_details() + + self.assertEqual(details['name'], 'test_project') + self.assertEqual(details['path'], self.temp_dir) + self.assertEqual(details['venv_name'], 'venv') + + @patch('subprocess.run') + def test_install_requirements(self, mock_run): + """Test installing requirements""" + mock_run.return_value = MagicMock(stderr="", returncode=0) + + # Create test requirements file + with open(self.test_requirements, 'w') as f: + f.write('requests==2.26.0\ndjango==3.2.5') + + venv_path = os.path.join(self.temp_dir, 'venv') + os.makedirs(venv_path) + + result = self.venv_creator.install_requirements(venv_path, self.test_requirements) + + self.assertTrue(result) + mock_run.assert_called_once() + + def test_find_python_installations(self): + """Test finding Python installations""" + installations = self.venv_creator.find_python_installations() + self.assertIsInstance(installations, list) + if installations: + self.assertTrue(all(os.path.exists(path) for path in installations)) + + @patch('builtins.print') + def test_print_activation_instructions(self, mock_print): + """Test printing activation instructions""" + project_path = "/test/path" + venv_name = "venv" + + self.venv_creator.print_activation_instructions(project_path, venv_name) + mock_print.assert_called() + +if __name__ == '__main__': + unittest.main() \ No newline at end of file