Skip to content
Draft
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
6 changes: 3 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ Core facts:

```bash
black --check --line-length=100 mssql_python/ tests/ # BLOCKING in CI
python -m pytest -v # 'stress' marker excluded by default
python -m pytest -v # 'stress' and 'typing' excluded by default
```

- **`pr-format-check` (BLOCKING):** PR title must start with one of `FEAT: FIX: DOC: CHORE: STYLE: REFACTOR: PERF: RELEASE: AI:`; the body must link a work item/issue and have a `### Summary` of at least 10 characters.
- Use `AI:` for AI tooling, agents, skills, prompts, and AI-assisted development workflows, not merely because AI helped write an ordinary fix or feature.
- `flake8`, `pylint`, `mypy`, `clang-format`, and `cpplint` run but are **informational**, not blocking.
- `flake8`, `pylint`, the GitHub lint workflow's `mypy` step, `clang-format`, and `cpplint` are **informational**. The separate source/stub typing harness (`python -m pytest tests/test_typing.py -m typing -v`) checks only `mssql_python` in strict mode and is **blocking** in the ADO PR-validation pipeline. `tests` and `mssql_python_odbc` are not typing-gate targets; the runtime test jobs are unchanged.
- The authoritative cross-platform validation runs on **Azure DevOps** (broader OS / Python / arch coverage than the GitHub checks); consult the specific pipeline in `eng/pipelines/` for the exact matrix rather than assuming full coverage. A coverage bot posts a report comment on the PR.

## Code standards
Expand All @@ -64,7 +64,7 @@ python -m pytest -v # 'stress' marker excl

## Testing conventions

- Test files are mostly numbered `test_NNN_*.py`; `tests/test_000_dependencies.py` runs without a DB, most others need a live SQL Server. `-m "not stress"` is the default.
- Test files are mostly numbered `test_NNN_*.py`; `tests/test_000_dependencies.py` runs without a DB, most others need a live SQL Server. `-m "not stress and not typing"` is the default; run source/stub typing checks separately with `python -m pytest tests/test_typing.py -m typing -v` (no DB required).
- Run segfault-prone or ODBC/pool global-state tests in an **isolated subprocess** so a crash or shared state cannot poison the rest of the suite.
- **Assert the contract, not just the output.** If a change's value is "we now call X once," assert the call/round-trip count — a correctness-only test won't catch a perf regression.
- For global type-mapping changes, add typed-NULL integration cases (VARBINARY, UNIQUEIDENTIFIER, XML, DECIMAL, stored-proc params) before applying the optimization broadly.
Expand Down
24 changes: 16 additions & 8 deletions .github/prompts/run-tests.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ python -c "import pytest; print('✅ pytest ready:', pytest.__version__)"
pip install pytest pytest-cov
```

The typing harness checks only `mssql_python` source/stub files, not `tests` or
`mssql_python_odbc`, and does not require a database. After building the native
extension, run `python -m pytest tests/test_typing.py -m typing -v`
without completing the database checks below.

### Step 3: Verify Database Connection String

```bash
Expand Down Expand Up @@ -126,33 +131,34 @@ Help the developer run tests to validate their changes. Follow this process base

| Category | Description | When to Use |
|----------|-------------|-------------|
| **All tests** | Full test suite (excluding stress) | Before creating PR |
| **Default tests** | Test suite excluding stress and separately gated typing tests | Before creating PR |
| **Specific file** | Single test file | Testing one area |
| **Specific test** | Single test function | Debugging a failure |
| **Stress tests** | Long-running, resource-intensive | Performance validation |
| **Typing tests** | Strict checking of `mssql_python` sources/stubs only; no database required | Python typing changes |
| **With coverage** | Tests + coverage report | Checking coverage |

### Ask the Developer

> "What would you like to test?"
> 1. **All tests** - Run full suite (recommended before PR)
> 1. **Default tests** - Run the suite excluding stress and separately gated typing tests
> 2. **Specific tests** - Tell me which file(s) or test name(s)
> 3. **With coverage** - Generate coverage report

---

## STEP 2: Run Tests

### Option A: Run All Tests (Default - Excludes Stress Tests)
### Option A: Run Default Tests (Excludes Stress and Typing Tests)

```bash
# From repository root
python -m pytest -v

# This automatically applies: -m "not stress" (from pytest.ini)
# This automatically applies: -m "not stress and not typing" (from pytest.ini)
```

### Option B: Run All Tests Including Stress Tests
### Option B: Run All Tests Including Stress and Typing Tests

```bash
python -m pytest -v -m ""
Expand Down Expand Up @@ -384,7 +390,7 @@ python -m pytest tests/ -v
### Common Commands

```bash
# Run all tests (default, excludes stress)
# Run default tests (excludes stress and separately gated typing tests)
python -m pytest -v

# Run specific file
Expand Down Expand Up @@ -417,9 +423,11 @@ The project uses these default settings in `pytest.ini`:
[pytest]
markers =
stress: marks tests as stress tests (long-running, resource-intensive)
slow: marks tests as extra-slow (sustained load, multi-minute duration)
typing: static mssql_python source and stub type checks (run separately in PR validation)

# Default: Skips stress tests
addopts = -m "not stress"
# Default: Skips stress and separately gated typing tests
addopts = -m "not stress and not typing"
```

---
Expand Down
39 changes: 39 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,42 @@ All pull requests must include:
Use `AI:` for changes to AI tooling, agents, skills, prompts, or AI-assisted
development workflows. It describes the subject of the change, not whether
AI helped write it; ordinary driver fixes and features keep their usual prefixes.

## Type Checking

The typing harness checks Python source (`.py`) and stub (`.pyi`) files recursively
under `mssql_python` only, excluding generated `build` directories and their
downloaded third-party sources. `tests` and `mssql_python_odbc` are not targets of
this typing gate. It does not execute the checked code or require a live database;
the existing runtime test jobs are unchanged.

After installing `requirements.txt` and building the native extension using
`.github/prompts/build-ddbc.prompt.md`, run:

```console
python -m pytest tests/test_typing.py -m typing -v
```

The harness runs mypy in strict mode, including checks inside unannotated function
bodies. It reports source, stub, and import errors without suppressing errors in
the driver. Explicit package bases resolve module names from the repository root.
Keep the mypy version pinned in `requirements.txt` aligned
with the existing locked development dependencies.

To run the same package check directly:

```console
python -m mypy --config-file= --strict --explicit-package-bases --exclude "(^|/)build/" --no-incremental mssql_python
```

Private `_ddbc_types.pyi` and `_pycore_types.pyi` declarations describe the native
boundaries; they do not replace or hide the Python implementations from mypy.
Keep these declarations aligned with the C++/Rust APIs, and keep the static
constant declarations aligned with the dynamically exported integer aliases.
The dependency tests check the native export names and constant declaration parity.

The existing required `MSSQL-Python-PR-Validation` pipeline runs the harness once,
in the Ubuntu CodeQL job immediately after its native build. Typing failures fail
the pipeline and block PR merging; no separate GitHub workflow or required check
is needed. The `typing` marker is excluded from default pytest runs so the same
static checks are not repeated across the database/OS matrix.
12 changes: 12 additions & 0 deletions eng/pipelines/pr-validation-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ jobs:
./build.sh
displayName: 'Build C++ extension for CodeQL analysis'

- script: |
python -m pytest tests/test_typing.py -m typing -v --junitxml=typing-test-results.xml
displayName: 'Gate mssql_python source and stub typing'

- task: PublishTestResults@2
condition: succeededOrFailed()
inputs:
testResultsFiles: 'typing-test-results.xml'
testRunTitle: 'mssql_python source and stub typing (Linux x64)'
failTaskOnFailedTests: true
displayName: 'Publish typing regression results'

- task: CodeQL3000Finalize@0
condition: always()
displayName: 'Finalize CodeQL'
Expand Down
12 changes: 6 additions & 6 deletions mssql_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
from .odbc_provider import ProviderManager


def get_native_provider_info() -> dict:
def get_native_provider_info() -> dict[str, object]:
"""Return the selected native provider for diagnostics.

Reports the provider ``id``, package ``version``, resolved ``driver_path``,
Expand All @@ -91,17 +91,17 @@ def get_native_provider_info() -> dict:


# Global registry for tracking active connections (using weak references)
_active_connections = weakref.WeakSet()
_active_connections: weakref.WeakSet[Connection] = weakref.WeakSet()
_connections_lock = threading.Lock()


def _register_connection(conn):
def _register_connection(conn: Connection) -> None:
"""Register a connection for cleanup before shutdown."""
with _connections_lock:
_active_connections.add(conn)


def _cleanup_connections():
def _cleanup_connections() -> None:
"""
Cleanup function called by atexit to close all active connections.

Expand Down Expand Up @@ -579,7 +579,7 @@ def pooling(max_size: int = 100, idle_timeout: int = 600, enabled: bool = True)
_original_module_setattr = sys.modules[__name__].__setattr__


def _custom_setattr(name, value):
def _custom_setattr(name: str, value: object) -> None:
if name == "lowercase":
with _settings_lock:
_settings.lowercase = bool(value)
Expand All @@ -590,7 +590,7 @@ def _custom_setattr(name, value):


# Replace the module's __setattr__ with our custom version
sys.modules[__name__].__setattr__ = _custom_setattr
setattr(sys.modules[__name__], "__setattr__", _custom_setattr)


# Create a custom module class that uses properties instead of __setattr__
Expand Down
Loading
Loading