diff --git a/README.md b/README.md index b1736260..540a5f9b 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,20 @@ The ctl extra provides the `infrahubctl` command, which allows you to intera pip install 'infrahub-sdk[ctl]' ``` -#### tests +#### testing -The tests extra provides all the components for the testing framework of Transforms, Queries and Checks. +The testing extra provides the components for the testing framework of Transforms, Queries and Checks. ```bash -pip install 'infrahub-sdk[tests]' +pip install 'infrahub-sdk[testing]' +``` + +#### testcontainers + +The testcontainers extra provides infrahub_sdk.testing.docker, which starts a real Infrahub in containers so tests can run against a live instance. + +```bash +pip install 'infrahub-sdk[testcontainers]' ``` #### all diff --git a/changelog/+dependency-lower-bounds.changed.md b/changelog/+dependency-lower-bounds.changed.md index 7105a61d..8dd6b483 100644 --- a/changelog/+dependency-lower-bounds.changed.md +++ b/changelog/+dependency-lower-bounds.changed.md @@ -4,4 +4,4 @@ If you pin `pydantic` to 2.0, 2.0.1 or 2.0.2, installing the SDK now fails while `anyio` and `typing-extensions` are now installed as direct requirements of the SDK itself. It has always imported them but relied on other packages to pull them in, so a minimal or heavily constrained environment could end up with the SDK installed and unusable. No action is needed, installs simply become reliable. -`packaging` is now a direct requirement of the `tests` extra, which is where the `infrahub_sdk.testing` helpers that import it live. A plain `pip install infrahub-sdk` does not install it. +`packaging` is now a direct requirement of the `testcontainers` extra, which is where the `infrahub_sdk.testing.docker` helper that imports it lives. A plain `pip install infrahub-sdk` does not install it. diff --git a/changelog/+testing-extras.changed.md b/changelog/+testing-extras.changed.md new file mode 100644 index 00000000..345e3897 --- /dev/null +++ b/changelog/+testing-extras.changed.md @@ -0,0 +1,7 @@ +Two extras now cover testing, replacing the `tests` extra. + +`pip install 'infrahub-sdk[testing]'` installs pytest and enables the bundled `pytest-infrahub` plugin, which is what you want to test your own Transforms, Queries and Checks. `pip install 'infrahub-sdk[testcontainers]'` additionally provides `infrahub_sdk.testing.docker`, which starts a real Infrahub in containers so your tests can run against a live instance instead of mocked responses. + +They are kept apart because the container tooling is a much heavier install. `[testing]` adds four packages on top of a plain install; `[testcontainers]` adds around sixty, among them Docker, FastAPI and Prefect client libraries. Previously you would have had to take all of it to get either. + +The `tests` extra is gone. It stopped installing anything in 1.16.0 while the documentation carried on advertising it, so since then `pip install 'infrahub-sdk[tests]'` has warned about an unknown extra and installed only the base package. If you are coming from 1.15.2 or earlier, `[testing]` is the direct replacement for what `[tests]` gave you. diff --git a/changelog/+tests-extra.added.md b/changelog/+tests-extra.added.md deleted file mode 100644 index 576f1d53..00000000 --- a/changelog/+tests-extra.added.md +++ /dev/null @@ -1,5 +0,0 @@ -`pip install 'infrahub-sdk[tests]'` now works. The `tests` extra is described in the installation guide but was never actually published, so the command warned that no such extra existed and installed nothing beyond the base package. - -Install it if you use the `pytest-infrahub` plugin to test Transforms, Queries and Checks, or the `infrahub_sdk.testing` helpers. Previously you had to work out the missing requirements and declare them yourself. - -Be aware that it is a large install, adding around 61 packages on top of the base SDK, among them Docker, FastAPI and Prefect client libraries. These come from the `infrahub_sdk.testing` helpers, which run Infrahub in containers. If you only need the `infrahubctl` CLI, install `infrahub-sdk[ctl]` instead. `infrahub-sdk[all]` now covers both `ctl` and `tests`, so it pulls in considerably more than it used to. diff --git a/docs/docs/python-sdk/guides/installation.mdx b/docs/docs/python-sdk/guides/installation.mdx index e1bda11e..0e8d5740 100644 --- a/docs/docs/python-sdk/guides/installation.mdx +++ b/docs/docs/python-sdk/guides/installation.mdx @@ -29,19 +29,31 @@ pip install 'infrahub-sdk[ctl]' -### tests +### testing -The `tests` extra provides all the components for the testing framework of Transforms, Queries and Checks. +The `testing` extra provides the components for the testing framework of Transforms, Queries and Checks. It installs `pytest` and enables the bundled `pytest-infrahub` plugin, which discovers the `infrahub_tests` blocks in your repository's `.infrahub.yml` and turns them into test cases. ```shell -pip install 'infrahub-sdk[tests]' +pip install 'infrahub-sdk[testing]' +``` + + + +### testcontainers + + + +The `testcontainers` extra provides `infrahub_sdk.testing.docker`, which starts a real Infrahub in containers so your tests can run against a live instance rather than mocked responses. Inherit `TestInfrahubDockerClient` to get a client connected to that instance. + +```shell +pip install 'infrahub-sdk[testcontainers]' ``` :::note -This extra is much larger than the others. The `infrahub_sdk.testing` helpers run Infrahub in containers, which adds roughly 61 packages on top of the base SDK. Install the `ctl` extra on its own if you only need the `infrahubctl` command. +This extra is considerably larger than the others, since running Infrahub in containers pulls in Docker, FastAPI and Prefect client libraries. Install `testing` on its own if you only need to test Transforms, Queries and Checks. ::: diff --git a/docs/docs/python-sdk/introduction.mdx b/docs/docs/python-sdk/introduction.mdx index 718cbe58..50d3f049 100644 --- a/docs/docs/python-sdk/introduction.mdx +++ b/docs/docs/python-sdk/introduction.mdx @@ -120,7 +120,7 @@ Extras are available for additional functionality: ```bash uv add 'infrahub-sdk[ctl]' # Adds the infrahubctl CLI -uv add 'infrahub-sdk[tests]' # Adds the testing framework for transforms and checks +uv add 'infrahub-sdk[testing]' # Adds the testing framework for transforms and checks uv add 'infrahub-sdk[all]' # Everything ``` @@ -135,7 +135,7 @@ Extras are available for additional functionality: ```bash pip install 'infrahub-sdk[ctl]' # Adds the infrahubctl CLI -pip install 'infrahub-sdk[tests]' # Adds the testing framework for transforms and checks +pip install 'infrahub-sdk[testing]' # Adds the testing framework for transforms and checks pip install 'infrahub-sdk[all]' # Everything ``` diff --git a/pyproject.toml b/pyproject.toml index c189db16..4a1ca73e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,18 +57,23 @@ ctl = [ "ariadne-codegen==0.18.0", ] -# The user-facing testing surface: `infrahub_sdk.testing` and the bundled `pytest-infrahub` -# plugin. Both ship in the wheel but import packages that no other extra installs. Distinct from -# the `tests` dependency group below, which is the tooling used to test the SDK itself. -tests = [ +# The bundled `pytest-infrahub` plugin and the schemas under `infrahub_sdk.testing`, for writing +# tests against your own Transforms, Queries and Checks. +testing = [ + "pytest>=7.0", +] + +# `infrahub_sdk.testing.docker`, which starts a real Infrahub in containers. Kept apart from +# `testing` because it is the only module that needs it and it is a far heavier install. +testcontainers = [ + "infrahub-sdk[testing]", "infrahub-testcontainers>=1.7.3", "packaging>=21.0", - "pytest>=7.0", ] # Self-referential so it cannot drift out of sync with the extras it aggregates. all = [ - "infrahub-sdk[ctl,tests]", + "infrahub-sdk[ctl,testing,testcontainers]", ] [dependency-groups] diff --git a/tests/unit/test_packaging_metadata.py b/tests/unit/test_packaging_metadata.py index c05d9a7d..618a8f24 100644 --- a/tests/unit/test_packaging_metadata.py +++ b/tests/unit/test_packaging_metadata.py @@ -11,7 +11,7 @@ import ast import re import sys -from collections.abc import Container, Iterator +from collections.abc import Iterable, Iterator from dataclasses import dataclass from pathlib import Path from typing import Any, TypeGuard @@ -34,10 +34,13 @@ # Modules that ship in the wheel but are allowed to need an extra, mapped to the extra that # supplies them. `ctl/` is the CLI; `async_typer` and `graphql/plugin.py` sit outside it but are -# only reachable from it. Everything else must import on a plain install. +# only reachable from it. Everything else must import on a plain install, which is why the entries +# are per-file where a package is split: `testing/repository.py` needs nothing beyond the core +# dependencies, so it is deliberately absent and held to the base install. EXTRA_ONLY_MODULES = { "ctl": ("ctl/", "async_typer.py", "graphql/plugin.py"), - "tests": ("pytest_plugin/", "testing/"), + "testing": ("pytest_plugin/", "testing/schemas/"), + "testcontainers": ("testing/docker.py",), } # Operators that establish a floor. A requirement without one of these lets a resolver reach back @@ -137,13 +140,29 @@ def _requirement_cases() -> list[RequirementCase]: ] -def _declared_in(sections: Container[str]) -> set[str]: - """The distributions installed by the given sections of `pyproject.toml`.""" - return { - _normalize(requirement.name) - for requirement in (Requirement(case.requirement) for case in _requirement_cases() if case.section in sections) - if not _is_self_reference(requirement) - } +def _declared_in(sections: Iterable[str]) -> set[str]: + """The distributions installed by the given sections of `pyproject.toml`. + + Self-referential extras are followed, so asking for `testcontainers` also returns whatever + the `testing` extra it references installs. Without that, an extra defined by self-reference + would look as though it installed nothing. + """ + cases = _requirement_cases() + pending, seen, declared = [_normalize(section) for section in sections], set(), set() + while pending: + section = pending.pop() + if section in seen: + continue + seen.add(section) + for case in cases: + if _normalize(case.section) != section: + continue + requirement = Requirement(case.requirement) + if _is_self_reference(requirement): + pending.extend(_normalize(extra) for extra in requirement.extras) + else: + declared.add(_normalize(requirement.name)) + return declared def _surface_of(relative_path: Path) -> str | None: diff --git a/uv.lock b/uv.lock index d4a62c76..61ff7ef6 100644 --- a/uv.lock +++ b/uv.lock @@ -775,11 +775,14 @@ ctl = [ { name = "ruamel-yaml" }, { name = "typer" }, ] -tests = [ +testcontainers = [ { name = "infrahub-testcontainers" }, { name = "packaging" }, { name = "pytest" }, ] +testing = [ + { name = "pytest" }, +] [package.dev-dependencies] dev = [ @@ -847,17 +850,18 @@ requires-dist = [ { name = "graphql-core", specifier = ">=3.1,<3.3" }, { name = "httpx", specifier = ">=0.20" }, { name = "infrahub-testcontainers", marker = "extra == 'all'", specifier = ">=1.7.3" }, - { name = "infrahub-testcontainers", marker = "extra == 'tests'", specifier = ">=1.7.3" }, + { name = "infrahub-testcontainers", marker = "extra == 'testcontainers'", specifier = ">=1.7.3" }, { name = "jinja2", specifier = ">=3" }, { name = "netutils", specifier = ">=1.0.0" }, { name = "packaging", marker = "extra == 'all'", specifier = ">=21.0" }, - { name = "packaging", marker = "extra == 'tests'", specifier = ">=21.0" }, + { name = "packaging", marker = "extra == 'testcontainers'", specifier = ">=21.0" }, { name = "pyarrow", marker = "extra == 'all'", specifier = ">=14" }, { name = "pyarrow", marker = "extra == 'ctl'", specifier = ">=14" }, { name = "pydantic", specifier = ">=2.0.3,!=2.1.0,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.0" }, { name = "pytest", marker = "extra == 'all'", specifier = ">=7.0" }, - { name = "pytest", marker = "extra == 'tests'", specifier = ">=7.0" }, + { name = "pytest", marker = "extra == 'testcontainers'", specifier = ">=7.0" }, + { name = "pytest", marker = "extra == 'testing'", specifier = ">=7.0" }, { name = "pyyaml", specifier = ">=6" }, { name = "rich", specifier = ">=12" }, { name = "ruamel-yaml", marker = "extra == 'all'", specifier = ">=0.18" }, @@ -869,7 +873,7 @@ requires-dist = [ { name = "ujson", specifier = ">=5" }, { name = "whenever", specifier = ">=0.9.3,<0.10.0" }, ] -provides-extras = ["all", "ctl", "tests"] +provides-extras = ["all", "ctl", "testcontainers", "testing"] [package.metadata.requires-dev] dev = [