Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Publish to PyPI
on:
push:
branches:
- main
- 'v[0-9]+.[0-9]+'
workflow_dispatch:

jobs:
Expand Down Expand Up @@ -54,4 +54,4 @@ jobs:
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: python -m twine upload dist/*
run: python -m twine upload dist/*
32 changes: 32 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Tests

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
push:
branches:
- "**"
workflow_dispatch:

permissions:
contents: read

jobs:
tests:
name: Unit tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Oldest supported (requires-python) and current, so a version-specific
# regression cannot hide behind a single interpreter.
python-version: ["3.10", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- run: pip install -e ".[dev]"
- name: Run tests
run: pytest tests/ -q
2 changes: 1 addition & 1 deletion src/genvm_linter/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ def setup(version, contract, json_output):
click.echo(f"Latest: {version}")

if not json_output:
click.echo(f"Setting up GenVM SDK...")
click.echo("Setting up GenVM SDK...")

def progress(downloaded: int, total: int):
if not json_output and total > 0:
Expand Down
31 changes: 31 additions & 0 deletions src/genvm_linter/lint/ast_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Shared AST helpers for lint checks."""

import ast

CONTRACT_BASE_NAMES = frozenset(
{
"Contract",
"gl.Contract",
"gl.contract.Contract",
"genlayer.Contract",
"genlayer.contract.Contract",
}
)


def dotted_name(node: ast.expr) -> str | None:
"""Return a dotted name for Name/Attribute expressions."""
parts: list[str] = []
current = node
while isinstance(current, ast.Attribute):
parts.append(current.attr)
current = current.value
if not isinstance(current, ast.Name):
return None
parts.append(current.id)
return ".".join(reversed(parts))


def is_contract_subclass(node: ast.ClassDef) -> bool:
"""Return whether a class uses a supported GenLayer Contract base spelling."""
return any(dotted_name(base) in CONTRACT_BASE_NAMES for base in node.bases)
12 changes: 4 additions & 8 deletions src/genvm_linter/lint/safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from dataclasses import dataclass
from pathlib import Path

from .ast_utils import is_contract_subclass

# Modules that are forbidden in GenLayer contracts (non-deterministic)
FORBIDDEN_MODULES = frozenset({
"random",
Expand Down Expand Up @@ -171,14 +173,8 @@ def _get_call_name(self, node: ast.Call) -> str:
return ""

def _is_contract_class(self, node: ast.ClassDef) -> bool:
"""Check if a class inherits from gl.Contract / genlayer.Contract / Contract."""
for base in node.bases:
if isinstance(base, ast.Attribute) and base.attr == "Contract":
if isinstance(base.value, ast.Name) and base.value.id in ("gl", "genlayer"):
return True
if isinstance(base, ast.Name) and base.id == "Contract":
return True
return False
"""Check if the class inherits from a supported Contract base."""
return is_contract_subclass(node)

def _get_exception_name(self, node: ast.expr) -> str:
"""Extract the exception class name from a raise target."""
Expand Down
28 changes: 5 additions & 23 deletions src/genvm_linter/lint/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from dataclasses import dataclass
from pathlib import Path

from .ast_utils import is_contract_subclass


@dataclass
class StructureWarning:
Expand Down Expand Up @@ -50,19 +52,8 @@ def visit_ClassDef(self, node: ast.ClassDef):
self.is_contract_class = old_is_contract

def _is_contract_subclass(self, node: ast.ClassDef) -> bool:
"""Check if class inherits from gl.Contract or Contract."""
for base in node.bases:
if isinstance(base, ast.Attribute):
# gl.Contract
if (isinstance(base.value, ast.Name) and
base.value.id == "gl" and
base.attr == "Contract"):
return True
elif isinstance(base, ast.Name):
# Contract (direct import)
if base.id == "Contract":
return True
return False
"""Check if the class inherits from a supported Contract base."""
return is_contract_subclass(node)

def _check_contract_class(self, node: ast.ClassDef):
"""Check all rules for a contract class."""
Expand Down Expand Up @@ -296,16 +287,7 @@ def visit_ClassDef(self, node: ast.ClassDef):
self.generic_visit(node)

def _is_contract_subclass(self, node: ast.ClassDef) -> bool:
for base in node.bases:
if isinstance(base, ast.Attribute):
if (isinstance(base.value, ast.Name) and
base.value.id == "gl" and
base.attr == "Contract"):
return True
elif isinstance(base, ast.Name):
if base.id == "Contract":
return True
return False
return is_contract_subclass(node)

def _collect_storage_types(self, annotation: ast.expr | None):
"""Collect custom class types used in storage annotations."""
Expand Down
10 changes: 4 additions & 6 deletions src/genvm_linter/stubs.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
"""Generate type stubs for GenLayer SDK."""

import os
import shutil
import subprocess
import sys
from pathlib import Path

from .validate.artifacts import download_artifacts, get_latest_version
from .validate.sdk_loader import get_sdk_paths_from_genvmroot, extract_sdk_paths, parse_contract_header

CACHE_DIR = Path.home() / ".cache" / "genvm-linter" / "stubs"

Expand Down Expand Up @@ -332,7 +330,7 @@ def generate_stubs(

# Extract SDK
from .validate.sdk_loader import extract_sdk_paths
sdk_paths = extract_sdk_paths(tarball_path, {})
sdk_paths, _upgrade_notes = extract_sdk_paths(tarball_path, {})

if not sdk_paths:
raise RuntimeError("Could not extract SDK paths")
Expand All @@ -344,19 +342,19 @@ def generate_stubs(
stubs_path.mkdir(parents=True, exist_ok=True)

# Try to run stubgen
stubgen_success = False
try:
env = os.environ.copy()
env["PYTHONPATH"] = str(src_path)

result = subprocess.run(
subprocess.run(
[sys.executable, "-m", "mypy.stubgen", "-p", "genlayer", "-o", str(stubs_path)],
env=env,
capture_output=True,
text=True,
check=False,
)
stubgen_success = result.returncode == 0
except Exception:
# stubgen is best-effort; the stubs are created from scratch below.
pass

# Post-process or create from scratch
Expand Down
Loading
Loading