diff --git a/.ansible-lint b/.ansible-lint index a4a7318b..c74792ee 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -16,7 +16,6 @@ skip_list: - 'var-naming[no-role-prefix]' # Variable naming - 'var-naming[pattern]' # Variable naming patterns - 'no-free-form' # Avoid free-form syntax - some legacy usage - - 'key-order[task]' # Task key order - 'name[casing]' # Name casing - 'yaml[document-start]' # YAML document start - 'role-name' # Role naming convention - too many cloud-* roles @@ -24,9 +23,8 @@ skip_list: - 'name[missing]' # All tasks should be named - 113 issues to fix (temporary) warn_list: - - no-changed-when - - yaml[line-length] - - risky-file-permissions + - yaml[line-length] # Line length - informational only + - key-order[task] # Task key ordering - many existing violations, fix gradually # Enable additional rules enable_list: @@ -37,13 +35,8 @@ enable_list: - yaml[new-line-at-end-of-file] # Files should end with newline - jinja[invalid] # Invalid Jinja2 syntax (catches template errors) - jinja[spacing] # Proper spacing in Jinja2 expressions - -# Rules we're actively working on fixing -# Move these from skip_list to enable_list as we fix them -# - 'name[missing]' # All tasks should be named - 113 issues to fix -# - 'no-changed-when' # Commands should not change things -# - 'yaml[line-length]' # Line length limit -# - 'risky-file-permissions' # File permissions + - no-changed-when # Commands should declare changed_when + - risky-file-permissions # File tasks must have explicit mode verbosity: 1 diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index c9cd4a5a..00000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: Claude Code Review - -'on': - pull_request: - types: [opened, synchronize] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" - -jobs: - claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v6.0.2 - with: - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - use_sticky_comment: true - prompt: | - REPO: ${{ github.repository }} - PR NUMBER: ${{ github.event.pull_request.number }} - - IMPORTANT: Before posting your review, minimize any previous review comments from yourself to avoid cluttering the PR: - 1. List existing comments: gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments - 2. Find comments from "github-actions[bot]" that contain "Code Review" or review-related content in the body - 3. For each such comment, minimize it as OUTDATED using: - gh api graphql -f query='mutation($id: ID!) { minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) { minimizedComment { isMinimized } } }' -f id="" - - Then review this pull request and provide feedback on: - - Code quality and best practices - - Potential bugs or issues - - Performance considerations - - Security concerns - - Test coverage - - Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. - - Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. - - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options - # Note: gh api is needed for minimizing old comments (workaround for broken use_sticky_comment) - # See: https://github.com/anthropics/claude-code-action/issues/419 - claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(gh api:*),Bash(ansible-playbook * --syntax-check),Bash(ansible-lint *),Bash(ruff check *),Bash(yamllint *),Bash(shellcheck *),Bash(python -m pytest *)"' diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index 144cac0b..00000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,49 +0,0 @@ ---- -name: Claude Code - -'on': - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - claude: - if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - actions: read # Required for Claude to read CI results on PRs - steps: - - name: Checkout repository - uses: actions/checkout@v6.0.2 - with: - fetch-depth: 1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - - # This is an optional setting that allows Claude to read CI results on PRs - additional_permissions: | - actions: read - - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' - - # Add allowed tools for Algo project - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(ansible-playbook * --syntax-check),Bash(ansible-lint *),Bash(ruff check *),Bash(yamllint *),Bash(shellcheck *),Bash(python -m pytest *)"' diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 852f4063..da7661b8 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -46,7 +46,7 @@ jobs: dnsmasq \ qrencode \ openssl \ - linux-headers-$(uname -r) \ + "linux-headers-$(uname -r)" \ libxml2-utils \ dnsutils @@ -296,6 +296,6 @@ jobs: EOF # Test that config is readable - docker run --rm --entrypoint cat -v $(pwd)/test-data:/data algo:ci-test /data/config.cfg + docker run --rm --entrypoint cat -v "$(pwd)/test-data:/data" algo:ci-test /data/config.cfg echo "✓ Docker image built and basic tests passed" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1de57b49..438b9391 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,7 +1,10 @@ --- name: Lint -'on': [push, pull_request] +'on': + push: + branches: [main, master] + pull_request: permissions: contents: read @@ -59,11 +62,32 @@ jobs: - name: Setup Algo environment uses: ./.github/actions/setup-algo - - name: Run ruff + - name: Run ruff check run: | # Fast Python linter uv run --with ruff ruff check . + - name: Run ruff format check + run: | + # Verify consistent Python formatting + uv run --with ruff ruff format --check . + + python-types: + name: Python type checking + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.1 + with: + persist-credentials: false + + - name: Setup Algo environment + uses: ./.github/actions/setup-algo + + - name: Run ty check + run: | + # Type checking with ty + uv run --with ty ty check + shellcheck: name: Shell script linting runs-on: ubuntu-22.04 @@ -125,3 +149,36 @@ jobs: Write-Host '✓ PSScriptAnalyzer check passed' } " + + actionlint: + name: GitHub Actions linting + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.1 + with: + persist-credentials: false + + - name: Install actionlint + run: | + bash <(curl -sL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) + sudo mv actionlint /usr/local/bin/ + + - name: Run actionlint + run: | + actionlint .github/workflows/*.yml + + zizmor: + name: GitHub Actions security audit + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.1 + with: + persist-credentials: false + + - name: Install zizmor + run: | + pip install zizmor + + - name: Run zizmor + run: | + zizmor .github/workflows/ diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..5ef925fc --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,25 @@ +--- +name: Security + +'on': + push: + branches: [main, master] + pull_request: + +permissions: + contents: read + +jobs: + pip-audit: + name: Python dependency audit + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.1 + with: + persist-credentials: false + + - name: Setup Algo environment + uses: ./.github/actions/setup-algo + + - name: Run pip-audit + run: uv run --with pip-audit pip-audit diff --git a/.github/workflows/test-effectiveness.yml b/.github/workflows/test-effectiveness.yml index 3913a06a..399a180c 100644 --- a/.github/workflows/test-effectiveness.yml +++ b/.github/workflows/test-effectiveness.yml @@ -53,7 +53,7 @@ jobs: --label "test-effectiveness,maintenance" else # Update existing issue - gh issue comment $existing --body-file .metrics/test-effectiveness-report.md + gh issue comment "$existing" --body-file .metrics/test-effectiveness-report.md fi fi diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2952ecb1..cd22a20c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,13 @@ -# See https://pre-commit.com for more information +# See https://prek.j178.dev for more information --- # Apply to all files without committing: -# pre-commit run --all-files +# prek run --all-files # Update this file: -# pre-commit autoupdate +# prek auto-update repos: - # General file checks - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + # Use prek built-in hooks (faster, Rust-native) + - repo: builtin hooks: - id: check-yaml args: [--allow-multiple-documents] @@ -23,7 +22,7 @@ repos: # Python linting with ruff (fast, replaces many tools) - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.6 + rev: v0.14.14 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -31,7 +30,7 @@ repos: # YAML linting - repo: https://github.com/adrienverge/yamllint - rev: v1.35.1 + rev: v1.38.0 hooks: - id: yamllint args: [-c=.yamllint] @@ -39,7 +38,7 @@ repos: # Shell script linting - repo: https://github.com/shellcheck-py/shellcheck-py - rev: v0.10.0.1 + rev: v0.11.0.1 hooks: - id: shellcheck exclude: '.git/.*' @@ -47,6 +46,13 @@ repos: # Local hooks that use the project's installed tools - repo: local hooks: + - id: ty-check + name: Python type check + entry: bash -c 'uv run --with ty ty check' + language: system + types: [python] + pass_filenames: false + - id: ansible-lint name: Ansible-lint entry: bash -c 'uv run ansible-lint --force-color || echo "Ansible-lint had issues - check output"' @@ -63,9 +69,14 @@ repos: files: 'main\.yml|server\.yml|users\.yml' pass_filenames: false -# Configuration for the pre-commit tool itself -default_language_version: - python: python3.11 + - id: actionlint + name: GitHub Actions lint + entry: bash -c 'command -v actionlint >/dev/null && actionlint .github/workflows/ || echo "actionlint not installed - skipping"' + language: system + files: '^\.github/workflows/.*\.yml$' + pass_filenames: false + +# Configuration for prek # Files to exclude globally exclude: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c273161c..d1727046 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ * Clone the repository: `git clone https://github.com/trailofbits/algo.git` * Run Algo: `./algo` (dependencies installed automatically via uv) -* Install pre-commit hooks: `uv run pre-commit install` (optional, for contributors) +* Install git hooks: `prek install` (optional, for contributors) * For local testing, consider using Docker or a cloud provider test instance Thanks! diff --git a/README.md b/README.md index 93ed5164..f1b97482 100644 --- a/README.md +++ b/README.md @@ -268,7 +268,7 @@ If you've read all the documentation and have further questions, [create a new d See our [Development Guide](docs/DEVELOPMENT.md) for information on: * Setting up your development environment -* Using pre-commit hooks for code quality +* Using prek hooks for code quality * Running tests and linters * Contributing code via pull requests diff --git a/pyproject.toml b/pyproject.toml index d6ffa424..e2f38223 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,28 +56,67 @@ line-length = 120 [tool.ruff.lint] select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - "I", # isort - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "UP", # pyupgrade + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "S", # flake8-bandit (security) + "SIM", # flake8-simplify + "RUF", # Ruff-specific rules + "ERA", # commented-out code detection + "PTH", # pathlib recommendations ] ignore = [ - "E501", # line too long (handled by formatter) - "B011", # assert False is acceptable in test code + "E501", # line too long (handled by formatter) + "B011", # assert False is acceptable in test code + "S101", # assert is acceptable in test code + "S110", # try-except-pass - used intentionally for optional checks + "S112", # try-except-continue - used intentionally for skipping files + "S603", # subprocess calls - needed for Ansible modules + "S607", # partial path - needed for Ansible modules + "S701", # jinja2 autoescape - templates are for config files, not HTML + "S602", # shell=True in subprocess - needed for test mocks + "SIM102", # nested if - sometimes clearer than combined conditions + "SIM108", # ternary - sometimes if/else is more readable + "ERA001", # commented code - some comments explain regex patterns + "RUF005", # iterable unpacking - concatenation is clearer in some cases + "PTH100", # pathlib - existing code uses os.path + "PTH108", # pathlib - existing code uses os.unlink + "PTH110", # pathlib - existing code uses os.path.exists + "PTH118", # pathlib - existing code uses os.path.join + "PTH119", # pathlib - existing code uses os.path.basename + "PTH120", # pathlib - existing code uses os.path.dirname + "PTH123", # pathlib - existing code uses open() + "PTH201", # pathlib - existing code uses Path(".") + "PTH207", # pathlib - existing code uses glob ] [tool.ruff.lint.per-file-ignores] "library/*" = ["ALL"] # Exclude Ansible library modules (external code) +"tests/*" = ["S101"] # Allow assert in tests + +[tool.ty.environment] +# Type checking configuration +python-version = "3.11" + +[tool.ty.src] +# Exclude Ansible library modules and tests (test code has looser typing) +exclude = ["library/**", "tests/**"] + +[tool.ty.rules] +# Ignore import warnings - ty doesn't see the venv when run via uv --with +# These are checked by Python's import system at runtime +unresolved-import = "ignore" +unknown-argument = "warn" [tool.uv] # Centralized uv version management dev-dependencies = [ "pytest>=8.0.0", "pytest-xdist>=3.0.0", # Parallel test execution - "pre-commit>=4.0.0", # Git hooks for code quality "ruff>=0.8.0", # Python linter and formatter "yamllint>=1.35.0", # YAML linter "ansible-lint>=24.0.0", # Ansible linter diff --git a/tests/integration/ansible-service-wrapper.py b/tests/integration/ansible-service-wrapper.py index ed58405c..140ea5b2 100644 --- a/tests/integration/ansible-service-wrapper.py +++ b/tests/integration/ansible-service-wrapper.py @@ -39,7 +39,7 @@ for svc in known_services: if service_found: # Return success result = { - "changed": True if state in ["started", "stopped", "restarted", "reloaded"] else False, + "changed": state in ["started", "stopped", "restarted", "reloaded"], "name": service_name, "state": state, "status": { diff --git a/tests/test_cloud_init_template.py b/tests/test_cloud_init_template.py index 806fece6..1d119304 100644 --- a/tests/test_cloud_init_template.py +++ b/tests/test_cloud_init_template.py @@ -136,7 +136,7 @@ class TestCloudInitTemplate: # Verify proper formatting - first line should be Port directive lines = content.strip().split("\n") - assert lines[0].strip() == "Port 4160", f"First line should be 'Port 4160', got: {repr(lines[0])}" + assert lines[0].strip() == "Port 4160", f"First line should be 'Port 4160', got: {lines[0]!r}" print("✅ SSH configuration correct") @@ -220,7 +220,7 @@ class TestCloudInitTemplate: for line in non_empty_lines: # Each line should start with exactly 6 spaces assert line.startswith(" ") and not line.startswith(" "), ( - f"Line should have exactly 6 spaces indentation: {repr(line)}" + f"Line should have exactly 6 spaces indentation: {line!r}" ) print("✅ Indentation is consistent") diff --git a/tests/unit/test_ansible_12_boolean_fix.py b/tests/unit/test_ansible_12_boolean_fix.py index 35b3c6b2..bc4b19e2 100644 --- a/tests/unit/test_ansible_12_boolean_fix.py +++ b/tests/unit/test_ansible_12_boolean_fix.py @@ -20,13 +20,13 @@ class TestAnsible12BooleanFix: # Check that we're NOT using the broken pattern broken_pattern = r'ipv6_support:\s*".*\}true\{.*\}false\{.*"' - assert not re.search(broken_pattern, content), \ + assert not re.search(broken_pattern, content), ( "ipv6_support is using string literals 'true'/'false' which breaks Ansible 12" + ) # Check that we ARE using the correct pattern correct_pattern = r'ipv6_support:\s*".*is\s+defined.*"' - assert re.search(correct_pattern, content), \ - "ipv6_support should use 'is defined' which returns a boolean" + assert re.search(correct_pattern, content), "ipv6_support should use 'is defined' which returns a boolean" def test_input_yml_algo_variables_not_string_boolean(self): """Verify algo_* variables in input.yml don't produce string 'false'.""" @@ -37,16 +37,16 @@ class TestAnsible12BooleanFix: # Variables to check algo_vars = [ - 'algo_ondemand_cellular', - 'algo_ondemand_wifi', - 'algo_dns_adblocking', - 'algo_ssh_tunneling', - 'algo_store_pki' + "algo_ondemand_cellular", + "algo_ondemand_wifi", + "algo_dns_adblocking", + "algo_ssh_tunneling", + "algo_store_pki", ] for var in algo_vars: # Find the variable definition - var_pattern = rf'{var}:.*?\n(.*?)\n\s*algo_' + var_pattern = rf"{var}:.*?\n(.*?)\n\s*algo_" match = re.search(var_pattern, content, re.DOTALL) if match: @@ -54,28 +54,30 @@ class TestAnsible12BooleanFix: # Check that we're NOT using string literal 'false' # The broken pattern: {%- else %}false{% endif %} - assert not re.search(r'\{%-?\s*else\s*%\}false\{%', var_content), \ + assert not re.search(r"\{%-?\s*else\s*%\}false\{%", var_content), ( f"{var} is using string literal 'false' which breaks Ansible 12" + ) # Check that we ARE using {{ false }} # The correct pattern: {%- else %}{{ false }}{% endif %} - if 'else' in var_content: - assert '{{ false }}' in var_content or '{{ true }}' in var_content or '| bool' in var_content, \ + if "else" in var_content: + assert "{{ false }}" in var_content or "{{ true }}" in var_content or "| bool" in var_content, ( f"{var} should use '{{{{ false }}}}' or '{{{{ true }}}}' for boolean values" + ) def test_no_bare_true_false_in_templates(self): """Scan for any remaining bare 'true'/'false' in Jinja2 expressions.""" # Patterns that indicate string boolean literals (bad) bad_patterns = [ - r'\{%[^%]*\}true\{%', # %}true{% - r'\{%[^%]*\}false\{%', # %}false{% - r'%\}true\{%', # %}true{% - r'%\}false\{%', # %}false{% + r"\{%[^%]*\}true\{%", # %}true{% + r"\{%[^%]*\}false\{%", # %}false{% + r"%\}true\{%", # %}true{% + r"%\}false\{%", # %}false{% ] files_to_check = [ Path(__file__).parent.parent.parent / "roles/common/tasks/facts.yml", - Path(__file__).parent.parent.parent / "input.yml" + Path(__file__).parent.parent.parent / "input.yml", ] for file_path in files_to_check: @@ -84,9 +86,10 @@ class TestAnsible12BooleanFix: for pattern in bad_patterns: matches = re.findall(pattern, content) - assert not matches, \ - f"Found string boolean literal in {file_path.name}: {matches}. " \ + assert not matches, ( + f"Found string boolean literal in {file_path.name}: {matches}. " f"Use '{{{{ true }}}}' or '{{{{ false }}}}' instead." + ) def test_conditional_uses_of_variables(self): """Check that when: conditions using these variables will work with booleans.""" @@ -94,7 +97,7 @@ class TestAnsible12BooleanFix: files_to_check = [ Path(__file__).parent.parent.parent / "roles/common/tasks/iptables.yml", Path(__file__).parent.parent.parent / "server.yml", - Path(__file__).parent.parent.parent / "users.yml" + Path(__file__).parent.parent.parent / "users.yml", ] for file_path in files_to_check: @@ -105,14 +108,13 @@ class TestAnsible12BooleanFix: content = f.read() # Find when: conditions - when_patterns = re.findall(r'when:\s*(\w+)\s*$', content, re.MULTILINE) + when_patterns = re.findall(r"when:\s*(\w+)\s*$", content, re.MULTILINE) # These variables must be booleans for Ansible 12 - boolean_vars = ['ipv6_support', 'algo_dns_adblocking', 'algo_ssh_tunneling'] + boolean_vars = ["ipv6_support", "algo_dns_adblocking", "algo_ssh_tunneling"] for var in when_patterns: if var in boolean_vars: # This is good - we're using the variable directly # which requires it to be a boolean in Ansible 12 pass # Test passes if we get here - diff --git a/tests/unit/test_boolean_variables.py b/tests/unit/test_boolean_variables.py index c621ad5e..4671aa96 100644 --- a/tests/unit/test_boolean_variables.py +++ b/tests/unit/test_boolean_variables.py @@ -21,12 +21,12 @@ class TestBooleanVariables: """Ensure ipv6_support produces boolean, not string 'true'/'false'.""" # Test with gateway defined (should be boolean True) template = "{{ ansible_default_ipv6['gateway'] is defined }}" - vars_with_gateway = {'ansible_default_ipv6': {'gateway': 'fe80::1'}} + vars_with_gateway = {"ansible_default_ipv6": {"gateway": "fe80::1"}} result = render_template(template, vars_with_gateway) assert result == "True" # Jinja2 renders boolean True as string "True" # Test without gateway (should be boolean False) - vars_no_gateway = {'ansible_default_ipv6': {}} + vars_no_gateway = {"ansible_default_ipv6": {}} result = render_template(template, vars_no_gateway) assert result == "False" # Jinja2 renders boolean False as string "False" @@ -83,7 +83,7 @@ class TestBooleanVariables: # Simulate the boolean value in a conditional context # In Ansible 12, this would fail if it's a string "true"/"false" - vars_with_gateway = {'ansible_default_ipv6': {'gateway': 'fe80::1'}} + vars_with_gateway = {"ansible_default_ipv6": {"gateway": "fe80::1"}} ipv6_result = render_template(fixed_ipv6, vars_with_gateway) # The result should be "True" (boolean rendered), not "true" (string literal) @@ -118,4 +118,3 @@ class TestBooleanVariables: fixed_algo = "{% if var is defined %}{{ var | bool }}{%- else %}{{ false }}{% endif %}" assert "{}false{}" not in fixed_algo.replace(" ", "") assert "{{ false }}" in fixed_algo - diff --git a/tests/unit/test_comprehensive_boolean_scan.py b/tests/unit/test_comprehensive_boolean_scan.py index 3886200f..071632b5 100644 --- a/tests/unit/test_comprehensive_boolean_scan.py +++ b/tests/unit/test_comprehensive_boolean_scan.py @@ -49,36 +49,36 @@ class TestComprehensiveBooleanScan: # Define directories to scan (Algo's actual code) algo_dirs = [ - 'roles', - 'playbooks', - 'library', - 'files/cloud-init', # Include cloud-init templates but not CloudFormation + "roles", + "playbooks", + "library", + "files/cloud-init", # Include cloud-init templates but not CloudFormation ] # Add root-level YAML files - yaml_files.extend(root.glob('*.yml')) - yaml_files.extend(root.glob('*.yaml')) + yaml_files.extend(root.glob("*.yml")) + yaml_files.extend(root.glob("*.yaml")) # Add YAML files from Algo directories for dir_name in algo_dirs: dir_path = root / dir_name if dir_path.exists(): - yaml_files.extend(dir_path.glob('**/*.yml')) - yaml_files.extend(dir_path.glob('**/*.yaml')) + yaml_files.extend(dir_path.glob("**/*.yml")) + yaml_files.extend(dir_path.glob("**/*.yaml")) # Exclude patterns excluded = [ - '.venv', # Virtual environment - '.env', # Another virtual environment pattern - 'venv', # Yet another virtual environment - 'test', # Test files (but keep our own tests) - 'molecule', # Molecule test files - 'site-packages', # Python packages - 'ansible_collections', # External Ansible collections - 'stack.yaml', # CloudFormation templates (use string booleans by design) - 'stack.yml', # CloudFormation templates - '.git', # Git directory - '__pycache__', # Python cache + ".venv", # Virtual environment + ".env", # Another virtual environment pattern + "venv", # Yet another virtual environment + "test", # Test files (but keep our own tests) + "molecule", # Molecule test files + "site-packages", # Python packages + "ansible_collections", # External Ansible collections + "stack.yaml", # CloudFormation templates (use string booleans by design) + "stack.yml", # CloudFormation templates + ".git", # Git directory + "__pycache__", # Python cache ] # Filter out excluded paths and CloudFormation templates @@ -89,7 +89,7 @@ class TestComprehensiveBooleanScan: if any(exc in path_str for exc in excluded): continue # Skip CloudFormation templates in files/ directories - if '/files/' in path_str and f.name in ['stack.yaml', 'stack.yml']: + if "/files/" in path_str and f.name in ["stack.yaml", "stack.yml"]: continue filtered.append(f) @@ -114,7 +114,7 @@ class TestComprehensiveBooleanScan: """Check for bare 'false' after else in Jinja expressions.""" issues = [] # Pattern for {%- else %}false{% (should be {{ false }}) - pattern = re.compile(r'\{%-?\s*else\s*%\}(true|false)\{%') + pattern = re.compile(r"\{%-?\s*else\s*%\}(true|false)\{%") for yaml_file in self.get_yaml_files(): with open(yaml_file) as f: @@ -129,12 +129,12 @@ class TestComprehensiveBooleanScan: def test_when_conditions_use_booleans(self): """Verify 'when:' conditions that use our variables.""" boolean_vars = [ - 'ipv6_support', - 'algo_dns_adblocking', - 'algo_ssh_tunneling', - 'algo_ondemand_cellular', - 'algo_ondemand_wifi', - 'algo_store_pki' + "ipv6_support", + "algo_dns_adblocking", + "algo_ssh_tunneling", + "algo_ondemand_cellular", + "algo_ondemand_wifi", + "algo_store_pki", ] potential_issues = [] @@ -144,17 +144,18 @@ class TestComprehensiveBooleanScan: lines = f.readlines() for i, line in enumerate(lines): - if 'when:' in line: + if "when:" in line: for var in boolean_vars: if var in line: # Check if it's a simple condition (good) or comparing to string (bad) - if f'{var} == "true"' in line or f'{var} == "false"' in line: + if ( + f'{var} == "true"' in line + or f'{var} == "false"' in line + or f'{var} != "true"' in line + or f'{var} != "false"' in line + ): potential_issues.append( - f"{yaml_file.name}:{i+1}: Comparing {var} to string in when condition" - ) - elif f'{var} != "true"' in line or f'{var} != "false"' in line: - potential_issues.append( - f"{yaml_file.name}:{i+1}: Comparing {var} to string in when condition" + f"{yaml_file.name}:{i + 1}: Comparing {var} to string in when condition" ) assert not potential_issues, "Found string comparisons in when conditions:\n" + "\n".join(potential_issues) @@ -162,19 +163,19 @@ class TestComprehensiveBooleanScan: def test_template_files_boolean_usage(self): """Check Jinja2 template files for boolean usage.""" root = Path(__file__).parent.parent.parent - template_files = list(root.glob('**/*.j2')) + template_files = list(root.glob("**/*.j2")) issues = [] for template_file in template_files: - if '.venv' in str(template_file): + if ".venv" in str(template_file): continue with open(template_file) as f: content = f.read() # Check for conditionals using our boolean variables - if 'ipv6_support' in content: + if "ipv6_support" in content: # Look for string comparisons if 'ipv6_support == "true"' in content or 'ipv6_support == "false"' in content: issues.append(f"{template_file.name}: Comparing ipv6_support to string") @@ -192,7 +193,7 @@ class TestComprehensiveBooleanScan: root / "roles/common/tasks/iptables.yml", root / "server.yml", root / "users.yml", - root / "roles/dns/tasks/main.yml" + root / "roles/dns/tasks/main.yml", ] for test_file in test_files: @@ -203,11 +204,11 @@ class TestComprehensiveBooleanScan: content = f.read() # Find all when: conditions - when_lines = re.findall(r'when:\s*([^\n]+)', content) + when_lines = re.findall(r"when:\s*([^\n]+)", content) for when_line in when_lines: # Check if it's using one of our boolean variables - if any(var in when_line for var in ['ipv6_support', 'algo_dns_adblocking', 'algo_ssh_tunneling']): + if any(var in when_line for var in ["ipv6_support", "algo_dns_adblocking", "algo_ssh_tunneling"]): # Ensure it's not comparing to strings assert '"true"' not in when_line, f"String comparison in {test_file.name}: {when_line}" assert '"false"' not in when_line, f"String comparison in {test_file.name}: {when_line}" @@ -226,22 +227,21 @@ class TestComprehensiveBooleanScan: # Known safe exceptions in Algo safe_patterns = [ - 'booleans_map', # This maps string inputs to booleans - 'test_', # Test files may use different patterns - 'molecule', # Molecule tests - 'ANSIBLE_', # Environment variables are strings - 'validate_certs', # Some modules accept string booleans - 'Default:', # CloudFormation parameter defaults + "booleans_map", # This maps string inputs to booleans + "test_", # Test files may use different patterns + "molecule", # Molecule tests + "ANSIBLE_", # Environment variables are strings + "validate_certs", # Some modules accept string booleans + "Default:", # CloudFormation parameter defaults ] issues = [] for yaml_file in self.get_yaml_files(): # Skip files that aren't Ansible playbooks/tasks/vars - parts_to_check = ['tasks', 'vars', 'defaults', 'handlers', 'meta', 'playbooks'] - main_files = ['main.yml', 'users.yml', 'server.yml', 'input.yml'] - if not any(part in str(yaml_file) for part in parts_to_check) \ - and yaml_file.name not in main_files: + parts_to_check = ["tasks", "vars", "defaults", "handlers", "meta", "playbooks"] + main_files = ["main.yml", "users.yml", "server.yml", "input.yml"] + if not any(part in str(yaml_file) for part in parts_to_check) and yaml_file.name not in main_files: continue with open(yaml_file) as f: @@ -250,7 +250,7 @@ class TestComprehensiveBooleanScan: for i, line in enumerate(lines): # Skip comments and empty lines stripped_line = line.strip() - if not stripped_line or stripped_line.startswith('#'): + if not stripped_line or stripped_line.startswith("#"): continue for pattern, description in problematic_patterns: @@ -259,7 +259,7 @@ class TestComprehensiveBooleanScan: if not any(safe in line for safe in safe_patterns): # This is a real issue that would break Ansible 12 rel_path = yaml_file.relative_to(Path(__file__).parent.parent.parent) - issues.append(f"{rel_path}:{i+1}: {description} - {stripped_line}") + issues.append(f"{rel_path}:{i + 1}: {description} - {stripped_line}") # All Algo code should be fixed assert not issues, "Found boolean type issues that would break Ansible 12:\n" + "\n".join(issues[:10]) @@ -272,8 +272,8 @@ class TestComprehensiveBooleanScan: content = f.read() # Should use 'is defined', not string literals - assert 'is defined' in content, "facts.yml should use 'is defined'" - old_pattern = 'ipv6_support: "{% if ansible_default_ipv6[\'gateway\'] is defined %}' + assert "is defined" in content, "facts.yml should use 'is defined'" + old_pattern = "ipv6_support: \"{% if ansible_default_ipv6['gateway'] is defined %}" old_pattern += 'true{% else %}false{% endif %}"' assert old_pattern not in content, "facts.yml still has the old string boolean pattern" @@ -283,8 +283,8 @@ class TestComprehensiveBooleanScan: content = f.read() # Count occurrences of the fix - assert content.count('{{ false }}') >= 5, "input.yml should have at least 5 instances of {{ false }}" - assert '{%- else %}false{% endif %}' not in content, "input.yml still has bare 'false'" + assert content.count("{{ false }}") >= 5, "input.yml should have at least 5 instances of {{ false }}" + assert "{%- else %}false{% endif %}" not in content, "input.yml still has bare 'false'" def test_templates_handle_booleans_correctly(self): """Test that template files handle boolean variables correctly.""" @@ -304,12 +304,9 @@ class TestComprehensiveBooleanScan: if var_name in content: # Verify it's used in conditionals, not compared to strings - assert f'{var_name} == "true"' not in content, \ - f"{template_path} compares {var_name} to string 'true'" - assert f'{var_name} == "false"' not in content, \ - f"{template_path} compares {var_name} to string 'false'" + assert f'{var_name} == "true"' not in content, f"{template_path} compares {var_name} to string 'true'" + assert f'{var_name} == "false"' not in content, f"{template_path} compares {var_name} to string 'false'" # It should be used directly in if statements or with | bool filter - if f'if {var_name}' in content or f'{var_name} |' in content: + if f"if {var_name}" in content or f"{var_name} |" in content: pass # Good - using it as a boolean - diff --git a/tests/unit/test_double_templating.py b/tests/unit/test_double_templating.py index 1904c07c..274f81bd 100644 --- a/tests/unit/test_double_templating.py +++ b/tests/unit/test_double_templating.py @@ -25,10 +25,7 @@ def find_yaml_files() -> list[Path]: # Exclude test files and vendor directories excluded_dirs = {"venv", ".venv", "env", ".git", "__pycache__", ".pytest_cache"} - yaml_files = [ - f for f in yaml_files - if not any(excluded in f.parts for excluded in excluded_dirs) - ] + yaml_files = [f for f in yaml_files if not any(excluded in f.parts for excluded in excluded_dirs)] return sorted(yaml_files) @@ -52,16 +49,14 @@ def detect_double_templating(content: str) -> list[tuple[int, str]]: # This catches cases like value: "{{ '{{ var }}' }}" pattern3 = r"{{\s*['\"][^'\"]*{{[^}]*}}[^'\"]*['\"]" - lines = content.split('\n') + lines = content.split("\n") for i, line in enumerate(lines, 1): # Skip comments - stripped = line.split('#')[0] + stripped = line.split("#")[0] if not stripped.strip(): continue - if (re.search(pattern1, stripped) or - re.search(pattern2, stripped) or - re.search(pattern3, stripped)): + if re.search(pattern1, stripped) or re.search(pattern2, stripped) or re.search(pattern3, stripped): issues.append((i, line)) return issues diff --git a/tests/unit/test_lightsail_boto3_fix.py b/tests/unit/test_lightsail_boto3_fix.py index 2f4216e6..498d76cc 100644 --- a/tests/unit/test_lightsail_boto3_fix.py +++ b/tests/unit/test_lightsail_boto3_fix.py @@ -47,6 +47,8 @@ class TestLightsailBoto3Fix(unittest.TestCase): "lightsail_region_facts", os.path.join(os.path.dirname(__file__), "../../library/lightsail_region_facts.py"), ) + assert spec is not None, "Failed to create module spec" + assert spec.loader is not None, "Module spec has no loader" module = importlib.util.module_from_spec(spec) # This should not raise an error @@ -70,6 +72,8 @@ class TestLightsailBoto3Fix(unittest.TestCase): "lightsail_region_facts", os.path.join(os.path.dirname(__file__), "../../library/lightsail_region_facts.py"), ) + assert spec is not None, "Failed to create module spec" + assert spec.loader is not None, "Module spec has no loader" module = importlib.util.module_from_spec(spec) # Mock AnsibleModule diff --git a/tests/unit/test_openssl_compatibility.py b/tests/unit/test_openssl_compatibility.py index 62e937cc..998d3fbf 100644 --- a/tests/unit/test_openssl_compatibility.py +++ b/tests/unit/test_openssl_compatibility.py @@ -288,7 +288,7 @@ def validate_client_certificates_real(cert_files): # Check if this looks like a client cert vs server cert cn = certificate.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value # Server certs typically have IP addresses or domain names as CN - if not (cn.replace(".", "").isdigit() or "." in cn and len(cn.split(".")) == 4): + if not (cn.replace(".", "").isdigit() or ("." in cn and len(cn.split(".")) == 4)): client_certs.append((cert_path, certificate)) if not client_certs: @@ -386,7 +386,7 @@ def validate_pkcs12_files_real(cert_files): print("⚠ No PKCS#12 files found") return - major, minor = test_openssl_version_detection() + major, _minor = test_openssl_version_detection() for p12_file in cert_files["p12_files"]: assert os.path.exists(p12_file), f"PKCS#12 file should exist: {p12_file}" diff --git a/tests/unit/test_scaleway_fix.py b/tests/unit/test_scaleway_fix.py index 41cf63e8..bdeec4d2 100644 --- a/tests/unit/test_scaleway_fix.py +++ b/tests/unit/test_scaleway_fix.py @@ -29,9 +29,9 @@ def test_scaleway_main_uses_project_parameter(): content = f.read() # Should NOT use the broken scaleway_organization_info module - assert ( - "scaleway_organization_info" not in content - ), "Still using broken scaleway_organization_info module (issue #14846)" + assert "scaleway_organization_info" not in content, ( + "Still using broken scaleway_organization_info module (issue #14846)" + ) # Should NOT use the broken scaleway_image_info module assert "scaleway_image_info" not in content, "Still using broken scaleway_image_info module" @@ -64,9 +64,9 @@ def test_scaleway_prompts_collect_org_id(): assert "algo_scaleway_org_id:" in content, "Missing algo_scaleway_org_id fact definition" # Should support SCW_DEFAULT_ORGANIZATION_ID env var - assert ( - "SCW_DEFAULT_ORGANIZATION_ID" in content - ), "Missing support for SCW_DEFAULT_ORGANIZATION_ID environment variable" + assert "SCW_DEFAULT_ORGANIZATION_ID" in content, ( + "Missing support for SCW_DEFAULT_ORGANIZATION_ID environment variable" + ) # Should mention console.scaleway.com for finding the ID assert "console.scaleway.com" in content, "Missing instructions on where to find Organization ID" diff --git a/tests/unit/test_strongswan_templates.py b/tests/unit/test_strongswan_templates.py index 943e7592..adf240c9 100644 --- a/tests/unit/test_strongswan_templates.py +++ b/tests/unit/test_strongswan_templates.py @@ -162,8 +162,8 @@ def test_strongswan_templates(): print(f" ✅ {template_name} ({scenario})") except Exception as e: - errors.append(f"{template_path} ({scenario}): {str(e)}") - print(f" ❌ {template_name} ({scenario}): {str(e)}") + errors.append(f"{template_path} ({scenario}): {e!s}") + print(f" ❌ {template_name} ({scenario}): {e!s}") if errors: print(f"\n❌ StrongSwan template tests failed with {len(errors)} errors") @@ -296,8 +296,8 @@ def test_mobileconfig_template(): print(f" ✅ Mobileconfig: {test_case['name']}") except Exception as e: - errors.append(f"Mobileconfig ({test_case['name']}): {str(e)}") - print(f" ❌ Mobileconfig ({test_case['name']}): {str(e)}") + errors.append(f"Mobileconfig ({test_case['name']}): {e!s}") + print(f" ❌ Mobileconfig ({test_case['name']}): {e!s}") if errors: return False diff --git a/tests/unit/test_yaml_jinja2_expressions.py b/tests/unit/test_yaml_jinja2_expressions.py index 946b7054..c93fd471 100644 --- a/tests/unit/test_yaml_jinja2_expressions.py +++ b/tests/unit/test_yaml_jinja2_expressions.py @@ -155,7 +155,7 @@ def validate_jinja2_expression(expression, context_vars=None): error_str = str(e).lower() if any(ignore in error_str for ignore in ["undefined", "has no attribute", "no filter"]): return True, None # These are runtime issues, not syntax issues - return False, f"Error: {str(e)}" + return False, f"Error: {e!s}" def get_test_variables(): diff --git a/tests/validate_jinja2_templates.py b/tests/validate_jinja2_templates.py index 1adfebdd..b2ae44b1 100755 --- a/tests/validate_jinja2_templates.py +++ b/tests/validate_jinja2_templates.py @@ -172,7 +172,7 @@ def validate_template_syntax(template_path: Path) -> tuple[bool, list[str]]: except UnicodeDecodeError: errors.append(f"{template_path}: Unable to decode file (not UTF-8)") except Exception as e: - errors.append(f"{template_path}: Error: {str(e)}") + errors.append(f"{template_path}: Error: {e!s}") return len(errors) == 0, errors diff --git a/uv.lock b/uv.lock index ef39317a..01b64f8d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.12'", @@ -65,7 +65,6 @@ openstack = [ [package.dev-dependencies] dev = [ { name = "ansible-lint" }, - { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-xdist" }, { name = "ruff" }, @@ -98,7 +97,6 @@ provides-extras = ["aws", "azure", "gcp", "hetzner", "linode", "openstack", "clo [package.metadata.requires-dev] dev = [ { name = "ansible-lint", specifier = ">=24.0.0" }, - { name = "pre-commit", specifier = ">=4.0.0" }, { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-xdist", specifier = ">=3.0.0" }, { name = "ruff", specifier = ">=0.8.0" }, @@ -420,15 +418,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] -[[package]] -name = "cfgv" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, -] - [[package]] name = "charset-normalizer" version = "3.4.2" @@ -594,15 +583,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, ] -[[package]] -name = "distlib" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, -] - [[package]] name = "dogpile-cache" version = "1.4.0" @@ -672,15 +652,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/8e/db77671852393bb0c13004734e9486fc303e09764d1bac2d463f878adf65/hcloud-2.15.0-py3-none-any.whl", hash = "sha256:170ad5ae4344b53bb5926187cfea6df3b76e447d8b8f52dd9e0d1579632d076b", size = 111725, upload-time = "2026-01-16T10:20:31.463Z" }, ] -[[package]] -name = "identify" -version = "2.6.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243, upload-time = "2025-08-09T19:35:00.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153, upload-time = "2025-08-09T19:34:59.1Z" }, -] - [[package]] name = "idna" version = "3.10" @@ -951,15 +922,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/cc/f4fe2c7ce68b92cbf5b2d379ca366e1edae38cccaad00f69f529b460c3ef/netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe", size = 2262023, upload-time = "2024-05-28T21:30:34.191Z" }, ] -[[package]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, -] - [[package]] name = "oauthlib" version = "3.3.1" @@ -1061,22 +1023,6 @@ version = "0.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8f/c5/4249317962180d97ec7a60fe38aa91f86216533bd478a427a5468945c5c9/polling-0.3.2.tar.gz", hash = "sha256:3afd62320c99b725c70f379964bf548b302fc7f04d4604e6c315d9012309cc9a", size = 5189, upload-time = "2021-05-22T19:48:41.466Z" } -[[package]] -name = "pre-commit" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, -] - [[package]] name = "psutil" version = "7.0.0" @@ -1594,20 +1540,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] -[[package]] -name = "virtualenv" -version = "20.36.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, -] - [[package]] name = "wcmatch" version = "10.1"