Skip to content

Dev setup

Get from a fresh clone to a green make check in five minutes. Skim once; come back when CI surprises you.

Get from a fresh clone to a green make check in five minutes. Then this page is what you come back to when CI flags something you didn’t expect.

Prerequisites

  • Python 3.12+ — the project pins requires-python = ">=3.12".
  • uv — the package manager and task runner used here. pip/pip-tools workflows will diverge from CI; use uv.
  • git with push access if you intend to open PRs.

You do not need Netlab or Containerlab installed to develop the server itself — CI doesn’t have them either, and the test suite stubs LabManager to keep tests host-agnostic. See Internals: CI test stubbing.

Clone and install

git clone git@github.com:zebbra/remote-lab.git
cd neops-remote-lab

Then bring in the dependencies. The project declares its dev tooling under [dependency-groups] (PEP 735) in pyproject.toml; uv is the supported path and the one CI uses. Two alternatives exist for environments where uv is unavailable.

uv sync --group dev

Brings in ruff, pyrefly, pytest, pytest-asyncio, httpx, pip-audit, and the type stubs make check needs. Locks them in uv.lock. See the uv dependency-groups docs.

python -m venv .venv && source .venv/bin/activate
pip install --group dev .

pip 25.0+ supports --group natively for PEP 735. You won’t get the lockfile that uv produces; rely on pyproject.toml for reproducibility.

Poetry does not natively read [dependency-groups] (it uses its own [tool.poetry.group.<name>.dependencies] format). To work with this repo, install the runtime deps via poetry install and add the dev tools manually:

poetry install
poetry add --group dev ruff pyrefly pytest pytest-asyncio httpx \
    pip-audit fastapi[standard] types-requests

This is the highest-friction option. If you can install uv, do that instead — make check and CI will agree with you.

The make check pipeline

make check is what CI runs. Run it locally before every push:

make check

It chains four steps:

Target Command Failure mode
lint ruff format --check and ruff check Formatting differences or lint rule violations. Run make format to auto-fix the easy ones.
typeCheck pyrefly check Missing type annotations on public functions, type mismatches, untyped Any leakage.
audit pip-audit --strict A dependency has a known CVE. Bump it, preserve the # CVE-* pin comment, re-run.
test pytest Anything in tests/. CI uses the same command.

If any step fails, fix it locally before pushing — CI will not be more lenient.

What make format will and won’t do

make format runs ruff format and ruff check --fix, which auto-fixes formatting and a subset of lint rules (import order, unused imports, obvious style). It will not add missing type annotations or fix rule classes that need human judgment (security, complexity).

Code style at a glance

Defined in pyproject.toml under [tool.ruff]:

  • Line length 120, double quotes, 4-space indent, target Python 3.12.
  • Docstrings: Google style. The Pydocstyle convention is set to "google".
  • Tests (tests/**): docstrings not required, assert allowed, magic values allowed.
  • server.py: FastAPI Depends()/File() defaults are exempt from the “mutable default arg” rule.

Naming conventions that code review enforces:

  • Pydantic request/response models end in Dto (e.g., SessionInfoDto, LabStatusDto). This is invariant — see Invariants → *Dto suffix.
  • Module-private state has a leading underscore: _SESSIONS, _SESSION_QUEUE, _SHUTDOWN_EVENT.
  • Loggers: logging.getLogger(__name__) for library modules, logging.getLogger("remote-lab-server") for the server module.

Type checking

Pyrefly does the static type analysis. Configured in pyproject.toml:

  • Target: Python 3.12.
  • Scope: neops_remote_lab/**/*.py. Tests, docs, vendor scripts excluded.
  • untyped-def-behavior = "check-and-infer-return-type" — unannotated functions are checked with inferred return types; missing parameter or return annotations on public surfaces fail the check.

Run it on its own:

make typeCheck

If pyrefly objects to a Pydantic v2 BaseModel subclass with a misc-type warning, the established workaround is the # type: ignore[misc] comment on the class line — see existing models for the pattern.

CVE-pinned dependencies

Several pins in pyproject.toml carry a # CVE-* comment explaining why a minimum version exists:

"starlette>=0.49.1",   # CVE-2025-62727 fix
"filelock>=3.20.1,<4", # CVE-2025-68146 fix
"pytest>=9.0.3,<10",   # CVE-2025-71176 fix

When you upgrade a dependency that has one of these comments:

  1. Pick a version that still preserves the fix (a higher version that includes the CVE patch is fine; a lower version is not).
  2. Keep the comment on the line, possibly updated if the CVE numbering changes.
  3. Re-run make audit to confirm pip-audit --strict is still clean.
  4. Mention the dep bump in your commit message.

This is one of the project’s hard invariants. See Invariants → CVE-pinned dependencies.

Branch and PR flow

  • Branch from develop, never from main. main receives merges from develop at release time.
  • One logical change per PR. A bug fix is a PR. A refactor is a PR. Bundling them creates review noise.
  • Run make check locally before pushing. Anything CI catches is cheaper to fix before the push.
  • Write tests for new functionality. The test suite lives in tests/ and uses pytest plus pytest-asyncio. Use httpx.AsyncClient to exercise FastAPI endpoints; that’s the established pattern.

A typical PR looks like this:

# 1. Branch from develop
git fetch origin
git switch -c feature/my-change origin/develop

# 2. Make changes, test locally
$EDITOR neops_remote_lab/server.py
make format            # auto-fix what's easy
make check             # the same thing CI will run

# 3. Commit, push, open PR against develop
git add -p
git commit -m "feat: short summary, why-not-what in the body"
git push -u origin feature/my-change
gh pr create --base develop

Reviewers will look for: invariants preserved, tests added or updated, no # type: ignore introduced without a comment explaining why, CVE pin comments still in place if you touched dependencies.

Testing approach

CI cannot test paths that need a real Netlab installation; it runs on ubuntu-latest without netlab or Containerlab on PATH.

What CI does test:

  • FastAPI endpoint behavior — session management, request validation, error responses.
  • Session queue logic — promotion, cleanup, stale detection.
  • Model serialization and validation.
  • Client-side logic — retry, timeout, session lifecycle.

These tests rely on LabManager being stubbed during the test run; see Internals: CI test stubbing for the pattern.

What CI cannot test:

  • Real netlab up and topology startup.
  • Device inspection and connectivity through Netlab.
  • End-to-end remote-lab workflows with actual containers.

These have to run on a host with Netlab and Containerlab installed (the neops-labs VM, or a local Ubuntu machine that has been through Netlab host setup). Gate them with markers or skip conditions so they don’t fail in CI.

Pytest configuration

Configured in pyproject.toml under [tool.pytest.ini_options]:

  • asyncio_mode = "strict" — async tests must be explicitly marked @pytest.mark.asyncio. Implicit asyncio mode hides ordering bugs; strict catches them.
  • asyncio_default_fixture_loop_scope = "function" — each test gets its own loop. Don’t mix scopes without a reason.
  • --ignore=docs — the docs/ tree contains symlinks back into source (docs/neops_remote_lab/, docs/tests/) so mkdocs snippets can resolve source paths. pytest would otherwise try to collect them.

Pre-push checklist

Run this sequence before opening a PR:

make format            # auto-fix style issues
make check             # full CI pipeline (lint + type + audit + test)
git status             # confirm only intended changes staged
git diff origin/develop  # sanity-check the diff against the base branch

If make check is green and the diff is what you meant to write, push.

Where to go next

  • Invariants & internals — the rules a change cannot break, plus the internal mechanics that make the rules work. Read this before touching server.py, lab_manager.py, or anything in netlab/connector.py.
  • Architecture — the high-level component picture. Useful framing before the deep-dive in Invariants.