[{"data":1,"prerenderedAt":1547},["ShallowReactive",2],{"page-\u002Fadvanced-pytest-architecture-configuration\u002Fmanaging-conftest-hierarchies\u002Fcreating-conftestpy-hierarchies-for-monorepos\u002F":3},{"id":4,"title":5,"body":6,"description":1540,"extension":1541,"meta":1542,"navigation":265,"path":1543,"seo":1544,"stem":1545,"__hash__":1546},"content\u002Fadvanced-pytest-architecture-configuration\u002Fmanaging-conftest-hierarchies\u002Fcreating-conftestpy-hierarchies-for-monorepos\u002Findex.md","Creating conftest.py hierarchies for monorepos",{"type":7,"value":8,"toc":1514},"minimark",[9,13,25,30,39,56,74,91,95,103,106,166,184,205,209,215,225,230,233,403,407,414,509,512,562,566,573,577,594,600,623,627,630,831,835,847,869,873,883,887,897,969,976,987,1046,1050,1065,1069,1076,1080,1088,1178,1182,1195,1268,1272,1275,1306,1312,1315,1319,1473,1475,1479,1498,1510],[10,11,5],"h1",{"id":12},"creating-conftestpy-hierarchies-for-monorepos",[14,15,16,17,21,22,24],"p",{},"Scaling pytest across a multi-package monorepo introduces architectural complexity that flat test configurations cannot sustain. As test suites grow, implicit fixture resolution, unpredictable plugin loading, and collection bloat frequently degrade CI reliability and developer velocity. Establishing a deterministic ",[18,19,20],"code",{},"conftest.py"," hierarchy is not merely an organizational preference; it is a foundational requirement for maintaining strict scope boundaries, preventing cross-package leakage, and enabling parallelized execution. This guide details production-grade strategies for architecting, debugging, and validating ",[18,23,20],{}," hierarchies in modern Python monorepos.",[26,27,29],"h2",{"id":28},"monorepo-test-architecture-why-conftestpy-hierarchies-fail-at-scale","Monorepo Test Architecture: Why conftest.py Hierarchies Fail at Scale",[14,31,32,33,38],{},"When scaling pytest across a monorepo, developers frequently encounter unpredictable fixture resolution and silent collection failures. Understanding how pytest traverses directories and loads implicit plugins is foundational to avoiding architectural debt. The ",[34,35,37],"a",{"href":36},"\u002Fadvanced-pytest-architecture-configuration\u002F","Advanced Pytest Architecture & Configuration"," framework provides the necessary mental model for mapping collection boundaries to package structures.",[14,40,41,42,44,45,48,49,51,52,55],{},"At its core, pytest does not rely on Python's standard import system for test discovery. Instead, it executes an upward directory traversal starting from each collected test file. During this traversal, pytest identifies ",[18,43,20],{}," files and registers them as implicit plugins. These plugins are loaded into the ",[18,46,47],{},"FixtureManager"," in Last-In-First-Out (LIFO) order, meaning the nearest ",[18,50,20],{}," to the test file takes precedence over parent directories. While this mechanism simplifies local test execution, it becomes a liability in monorepos where multiple services share overlapping fixture names or where ",[18,53,54],{},"sys.path"," manipulation interferes with collection boundaries.",[14,57,58,59,61,62,65,66,69,70,73],{},"Collection bloat typically originates from flat ",[18,60,20],{}," structures. When a single root configuration file declares dozens of session-scoped fixtures, pytest eagerly evaluates them for every collected test, regardless of whether the test actually requests them. This eager evaluation compounds with ",[18,63,64],{},"autouse=True"," fixtures, triggering expensive database migrations, network stubs, or environment variable injections across unrelated packages. Furthermore, namespace package discovery (PEP 420) introduces subtle pitfalls: directories lacking ",[18,67,68],{},"__init__.py"," may be silently skipped during traversal, breaking expected fixture inheritance chains and causing ",[18,71,72],{},"FixtureLookupError"," exceptions that only manifest in CI environments.",[14,75,76,77,80,81,84,85,87,88,90],{},"The interaction between ",[18,78,79],{},"testpaths"," in ",[18,82,83],{},"pyproject.toml"," and ",[18,86,54],{}," resolution further complicates matters. If ",[18,89,79],{}," is misconfigured, pytest may inadvertently collect production modules, triggering import-time side effects or registering fixtures in unintended scopes. Without explicit boundary enforcement, monorepo test suites devolve into tightly coupled dependency graphs where modifying a single fixture in one package triggers cascading failures across unrelated services.",[26,92,94],{"id":93},"designing-the-hierarchy-root-domain-and-module-scopes","Designing the Hierarchy: Root, Domain, and Module Scopes",[14,96,97,98,102],{},"A resilient monorepo testing strategy requires explicit boundary enforcement. By delegating responsibilities across root, domain, and module tiers, teams can prevent cross-package fixture collisions. Detailed implementation patterns for this tiered approach are documented in ",[34,99,101],{"href":100},"\u002Fadvanced-pytest-architecture-configuration\u002Fmanaging-conftest-hierarchies\u002F","Managing Conftest Hierarchies",", which outlines scope delegation and safe override mechanics.",[14,104,105],{},"The three-tier delegation model maps directly to pytest's fixture scoping system:",[107,108,109,133,150],"ol",{},[110,111,112,120,121,124,125,128,129,132],"li",{},[113,114,115,116,119],"strong",{},"Root Tier (",[18,117,118],{},"\u002Ftests\u002Fconftest.py",")",": Handles global infrastructure, CI environment detection, and cross-cutting concerns. Fixtures here should be strictly ",[18,122,123],{},"session","-scoped and lazily initialized. This tier is the appropriate location for ",[18,126,127],{},"pytest_addoption"," hooks, global logging configuration, and baseline ",[18,130,131],{},"pytest_plugins"," declarations.",[110,134,135,141,142,145,146,149],{},[113,136,137,138,119],{},"Domain\u002FService Tier (",[18,139,140],{},"\u002Ftests\u002F\u003Cpackage>\u002Fconftest.py",": Encapsulates service-specific mocks, database schemas, and API client factories. Fixtures here typically operate at ",[18,143,144],{},"module"," or ",[18,147,148],{},"class"," scope. This tier enforces package isolation by overriding root fixtures with domain-specific implementations and applying marker-based filtering to prevent accidental cross-domain execution.",[110,151,152,158,159,162,163,165],{},[113,153,154,155,119],{},"Module\u002FEdge-Case Tier (",[18,156,157],{},"\u002Ftests\u002F\u003Cpackage>\u002Ftest_\u003Cmodule>\u002Fconftest.py",": Reserved for highly localized parametrization, temporary directory overrides, and test-specific state resets. These fixtures should remain ",[18,160,161],{},"function","-scoped and avoid ",[18,164,64],{}," unless strictly necessary for cleanup.",[14,167,168,169,171,172,175,176,179,180,183],{},"Strict scope isolation relies on explicit ",[18,170,131],{}," registration rather than implicit Python imports. Using ",[18,173,174],{},"import"," statements to share fixtures across packages bypasses pytest's internal dependency graph, causing fixtures to be registered in the wrong collection phase and breaking ",[18,177,178],{},"request.node"," introspection. Instead, declare shared fixture modules as strings in ",[18,181,182],{},"pytest_plugins = [\"tests.shared.fixtures.db\"]",". This ensures pytest loads them as proper plugins with correct hook execution order.",[14,185,186,187,80,189,191,192,194,195,197,198,200,201,204],{},"Additionally, configure ",[18,188,79],{},[18,190,83],{}," to restrict collection to dedicated test directories. Never place ",[18,193,68],{}," files inside test directories. Their presence converts test folders into Python packages, altering ",[18,196,54],{}," resolution and causing pytest to treat ",[18,199,20],{}," files as regular modules rather than implicit plugins. This distinction is critical for maintaining predictable ",[18,202,203],{},"pytest fixture scope isolation"," across large codebases.",[26,206,208],{"id":207},"implementation-minimal-reproducible-hierarchy","Implementation: Minimal Reproducible Hierarchy",[14,210,211,212,214],{},"The following directory structure demonstrates a production-ready hierarchy. Each ",[18,213,20],{}," file is scoped to its logical boundary, with explicit plugin registration replacing implicit import leakage.",[216,217,222],"pre",{"className":218,"code":220,"language":221},[219],"language-text","monorepo\u002F\n├── pyproject.toml\n├── src\u002F\n│ ├── auth\u002F\n│ └── billing\u002F\n└── tests\u002F\n ├── conftest.py # Root: Global CI & Plugin Registration\n ├── shared\u002F\n │ └── fixtures\u002F\n │ ├── db.py\n │ └── network.py\n ├── auth\u002F\n │ ├── conftest.py # Domain: Auth-specific overrides\n │ └── test_sessions.py\n └── billing\u002F\n ├── conftest.py # Domain: Billing-specific overrides\n └── test_invoices.py\n","text",[18,223,220],{"__ignoreMap":224},"",[226,227,229],"h3",{"id":228},"root-conftestpy-global-infrastructure-plugin-registration","Root conftest.py: Global Infrastructure & Plugin Registration",[14,231,232],{},"The root configuration establishes the baseline environment and registers shared fixture modules. It avoids heavy initialization, deferring expensive operations until explicitly requested.",[216,234,238],{"className":235,"code":236,"language":237,"meta":224,"style":224},"language-python shiki shiki-themes github-light github-dark","# tests\u002Fconftest.py\nimport pytest\nfrom pathlib import Path\n\n# Explicit plugin registration replaces cross-package imports\npytest_plugins = [\"tests.shared.fixtures.db\", \"tests.shared.fixtures.network\"]\n\ndef pytest_addoption(parser):\n \"\"\"Register CLI options for environment-specific toggles.\"\"\"\n parser.addoption(\n \"--env\", \n default=\"local\", \n choices=[\"local\", \"ci\", \"staging\"],\n help=\"Target environment for test execution\"\n )\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef global_config(request):\n \"\"\"Session-scoped configuration resolver.\n Uses request.config.getoption() to inject environment context.\n \"\"\"\n env = request.config.getoption(\"--env\")\n return {\n \"env\": env,\n \"db_url\": f\"sqlite:\u002F\u002F\u002Ftest_{env}.db\",\n \"mock_network\": env == \"ci\"\n }\n","python",[18,239,240,248,254,260,267,273,279,284,290,296,302,308,314,320,326,332,337,343,349,355,361,367,373,379,385,391,397],{"__ignoreMap":224},[241,242,245],"span",{"class":243,"line":244},"line",1,[241,246,247],{},"# tests\u002Fconftest.py\n",[241,249,251],{"class":243,"line":250},2,[241,252,253],{},"import pytest\n",[241,255,257],{"class":243,"line":256},3,[241,258,259],{},"from pathlib import Path\n",[241,261,263],{"class":243,"line":262},4,[241,264,266],{"emptyLinePlaceholder":265},true,"\n",[241,268,270],{"class":243,"line":269},5,[241,271,272],{},"# Explicit plugin registration replaces cross-package imports\n",[241,274,276],{"class":243,"line":275},6,[241,277,278],{},"pytest_plugins = [\"tests.shared.fixtures.db\", \"tests.shared.fixtures.network\"]\n",[241,280,282],{"class":243,"line":281},7,[241,283,266],{"emptyLinePlaceholder":265},[241,285,287],{"class":243,"line":286},8,[241,288,289],{},"def pytest_addoption(parser):\n",[241,291,293],{"class":243,"line":292},9,[241,294,295],{}," \"\"\"Register CLI options for environment-specific toggles.\"\"\"\n",[241,297,299],{"class":243,"line":298},10,[241,300,301],{}," parser.addoption(\n",[241,303,305],{"class":243,"line":304},11,[241,306,307],{}," \"--env\", \n",[241,309,311],{"class":243,"line":310},12,[241,312,313],{}," default=\"local\", \n",[241,315,317],{"class":243,"line":316},13,[241,318,319],{}," choices=[\"local\", \"ci\", \"staging\"],\n",[241,321,323],{"class":243,"line":322},14,[241,324,325],{}," help=\"Target environment for test execution\"\n",[241,327,329],{"class":243,"line":328},15,[241,330,331],{}," )\n",[241,333,335],{"class":243,"line":334},16,[241,336,266],{"emptyLinePlaceholder":265},[241,338,340],{"class":243,"line":339},17,[241,341,342],{},"@pytest.fixture(scope=\"session\", autouse=True)\n",[241,344,346],{"class":243,"line":345},18,[241,347,348],{},"def global_config(request):\n",[241,350,352],{"class":243,"line":351},19,[241,353,354],{}," \"\"\"Session-scoped configuration resolver.\n",[241,356,358],{"class":243,"line":357},20,[241,359,360],{}," Uses request.config.getoption() to inject environment context.\n",[241,362,364],{"class":243,"line":363},21,[241,365,366],{}," \"\"\"\n",[241,368,370],{"class":243,"line":369},22,[241,371,372],{}," env = request.config.getoption(\"--env\")\n",[241,374,376],{"class":243,"line":375},23,[241,377,378],{}," return {\n",[241,380,382],{"class":243,"line":381},24,[241,383,384],{}," \"env\": env,\n",[241,386,388],{"class":243,"line":387},25,[241,389,390],{}," \"db_url\": f\"sqlite:\u002F\u002F\u002Ftest_{env}.db\",\n",[241,392,394],{"class":243,"line":393},26,[241,395,396],{}," \"mock_network\": env == \"ci\"\n",[241,398,400],{"class":243,"line":399},27,[241,401,402],{}," }\n",[226,404,406],{"id":405},"domain-conftestpy-service-specific-overrides-isolation","Domain conftest.py: Service-Specific Overrides & Isolation",[14,408,409,410,413],{},"Domain-level configurations override root fixtures when necessary and enforce package boundaries using markers and ",[18,411,412],{},"autouse"," guards.",[216,415,417],{"className":235,"code":416,"language":237,"meta":224,"style":224},"# tests\u002Fauth\u002Fconftest.py\nimport pytest\n\n@pytest.fixture(scope=\"module\")\ndef service_client(global_config):\n \"\"\"Domain-scoped client factory.\n Inherits global_config but applies auth-specific initialization.\n \"\"\"\n return create_auth_client(global_config[\"db_url\"])\n\n@pytest.fixture(autouse=True)\ndef enforce_domain_boundary(request):\n \"\"\"Prevents cross-domain test execution without explicit markers.\n Uses request.node.keywords for marker introspection.\n \"\"\"\n if \"cross_domain\" not in request.node.keywords:\n yield\n else:\n pytest.skip(\"Cross-domain tests require explicit @pytest.mark.cross_domain\")\n",[18,418,419,424,428,432,437,442,447,452,456,461,465,470,475,480,485,489,494,499,504],{"__ignoreMap":224},[241,420,421],{"class":243,"line":244},[241,422,423],{},"# tests\u002Fauth\u002Fconftest.py\n",[241,425,426],{"class":243,"line":250},[241,427,253],{},[241,429,430],{"class":243,"line":256},[241,431,266],{"emptyLinePlaceholder":265},[241,433,434],{"class":243,"line":262},[241,435,436],{},"@pytest.fixture(scope=\"module\")\n",[241,438,439],{"class":243,"line":269},[241,440,441],{},"def service_client(global_config):\n",[241,443,444],{"class":243,"line":275},[241,445,446],{}," \"\"\"Domain-scoped client factory.\n",[241,448,449],{"class":243,"line":281},[241,450,451],{}," Inherits global_config but applies auth-specific initialization.\n",[241,453,454],{"class":243,"line":286},[241,455,366],{},[241,457,458],{"class":243,"line":292},[241,459,460],{}," return create_auth_client(global_config[\"db_url\"])\n",[241,462,463],{"class":243,"line":298},[241,464,266],{"emptyLinePlaceholder":265},[241,466,467],{"class":243,"line":304},[241,468,469],{},"@pytest.fixture(autouse=True)\n",[241,471,472],{"class":243,"line":310},[241,473,474],{},"def enforce_domain_boundary(request):\n",[241,476,477],{"class":243,"line":316},[241,478,479],{}," \"\"\"Prevents cross-domain test execution without explicit markers.\n",[241,481,482],{"class":243,"line":322},[241,483,484],{}," Uses request.node.keywords for marker introspection.\n",[241,486,487],{"class":243,"line":328},[241,488,366],{},[241,490,491],{"class":243,"line":334},[241,492,493],{}," if \"cross_domain\" not in request.node.keywords:\n",[241,495,496],{"class":243,"line":339},[241,497,498],{}," yield\n",[241,500,501],{"class":243,"line":345},[241,502,503],{}," else:\n",[241,505,506],{"class":243,"line":351},[241,507,508],{}," pytest.skip(\"Cross-domain tests require explicit @pytest.mark.cross_domain\")\n",[14,510,511],{},"Key implementation principles:",[513,514,515,531,539,552],"ul",{},[110,516,517,523,524,526,527,530],{},[113,518,519,520],{},"Placement outside ",[18,521,522],{},"src\u002F",": All ",[18,525,20],{}," files reside in the ",[18,528,529],{},"tests\u002F"," directory to prevent production import pollution.",[110,532,533,538],{},[113,534,535,537],{},[18,536,131],{}," syntax",": Always use list format. String concatenation or dynamic assignment breaks plugin discovery.",[110,540,541,544,545,547,548,551],{},[113,542,543],{},"Fixture override mechanics",": Child ",[18,546,20],{}," files automatically override parent fixtures with identical names. Use ",[18,549,550],{},"@pytest.fixture(autouse=True)"," cautiously to avoid unintended teardown side effects.",[110,553,554,557,558,561],{},[113,555,556],{},"Environment toggles",": ",[18,559,560],{},"request.config.getoption()"," enables deterministic behavior across local development and CI pipelines without modifying test code.",[26,563,565],{"id":564},"edge-case-resolution-cross-package-fixture-collisions-namespace-packages","Edge-Case Resolution: Cross-Package Fixture Collisions & Namespace Packages",[14,567,568,569,572],{},"When sibling packages define identically named fixtures, pytest resolves them based on collection order, leading to non-deterministic test behavior. Use ",[18,570,571],{},"pytest --fixtures --verbose"," to trace definition paths and enforce explicit naming conventions.",[226,574,576],{"id":575},"fixture-name-resolution-order","Fixture Name Resolution Order",[14,578,579,580,582,583,585,586,589,590,593],{},"Pytest's ",[18,581,47],{}," builds a dependency graph during collection. If two ",[18,584,20],{}," files in the same directory tree define ",[18,587,588],{},"@pytest.fixture(name=\"db_session\")",", the nearest file to the test file wins. However, if tests are collected in parallel or via glob patterns, the resolution order becomes unpredictable. This manifests as intermittent ",[18,591,592],{},"AssertionError"," failures where tests expect a mocked database but receive a production connection.",[14,595,596,599],{},[113,597,598],{},"Resolution Strategy",":",[107,601,602,613,620],{},[110,603,604,605,608,609,612],{},"Prefix fixtures with domain identifiers (e.g., ",[18,606,607],{},"auth_db_session",", ",[18,610,611],{},"billing_db_session",").",[110,614,615,616,619],{},"Use ",[18,617,618],{},"@pytest.fixture(name=\"explicit_name\")"," to enforce uniqueness.",[110,621,622],{},"Implement a CI linter to scan for duplicate fixture definitions before merge.",[226,624,626],{"id":625},"diagnostic-script-fixture-collision-detector","Diagnostic Script: Fixture Collision Detector",[14,628,629],{},"Automated collision detection prevents hierarchy drift. The following AST-based scanner identifies duplicate fixture names across the monorepo without executing tests.",[216,631,633],{"className":235,"code":632,"language":237,"meta":224,"style":224},"# scripts\u002Fscan_fixture_collisions.py\nimport ast\nimport sys\nfrom pathlib import Path\n\ndef scan_conftest_fixtures(root: Path) -> dict[str, list[str]]:\n \"\"\"Scan all conftest.py files and report duplicate fixture names.\"\"\"\n fixtures: dict[str, list[str]] = {}\n \n for conf in root.rglob(\"conftest.py\"):\n try:\n tree = ast.parse(conf.read_text())\n except SyntaxError:\n continue\n \n for node in ast.walk(tree):\n if isinstance(node, ast.FunctionDef):\n for dec in node.decorator_list:\n # Handle @pytest.fixture and @pytest.fixture(name=\"...\")\n if isinstance(dec, ast.Call) and hasattr(dec.func, \"attr\") and dec.func.attr == \"fixture\":\n # Check for explicit name kwarg\n name_kwarg = next((kw.value.value for kw in dec.keywords if kw.arg == \"name\"), None)\n fixture_name = name_kwarg or node.name\n fixtures.setdefault(fixture_name, []).append(str(conf.relative_to(root)))\n elif isinstance(dec, ast.Name) and dec.id == \"fixture\":\n fixtures.setdefault(node.name, []).append(str(conf.relative_to(root)))\n \n return {k: v for k, v in fixtures.items() if len(v) > 1}\n\nif __name__ == \"__main__\":\n root = Path(sys.argv[1] if len(sys.argv) > 1 else \".\")\n collisions = scan_conftest_fixtures(root)\n if collisions:\n print(\"️ Fixture collisions detected:\")\n for name, paths in collisions.items():\n print(f\" {name}: {', '.join(paths)}\")\n sys.exit(1)\n print(\"✅ No fixture collisions found.\")\n",[18,634,635,640,645,650,654,658,663,668,673,678,683,688,693,698,703,707,712,717,722,727,732,737,742,747,752,757,762,766,772,777,783,789,795,801,807,813,819,825],{"__ignoreMap":224},[241,636,637],{"class":243,"line":244},[241,638,639],{},"# scripts\u002Fscan_fixture_collisions.py\n",[241,641,642],{"class":243,"line":250},[241,643,644],{},"import ast\n",[241,646,647],{"class":243,"line":256},[241,648,649],{},"import sys\n",[241,651,652],{"class":243,"line":262},[241,653,259],{},[241,655,656],{"class":243,"line":269},[241,657,266],{"emptyLinePlaceholder":265},[241,659,660],{"class":243,"line":275},[241,661,662],{},"def scan_conftest_fixtures(root: Path) -> dict[str, list[str]]:\n",[241,664,665],{"class":243,"line":281},[241,666,667],{}," \"\"\"Scan all conftest.py files and report duplicate fixture names.\"\"\"\n",[241,669,670],{"class":243,"line":286},[241,671,672],{}," fixtures: dict[str, list[str]] = {}\n",[241,674,675],{"class":243,"line":292},[241,676,677],{}," \n",[241,679,680],{"class":243,"line":298},[241,681,682],{}," for conf in root.rglob(\"conftest.py\"):\n",[241,684,685],{"class":243,"line":304},[241,686,687],{}," try:\n",[241,689,690],{"class":243,"line":310},[241,691,692],{}," tree = ast.parse(conf.read_text())\n",[241,694,695],{"class":243,"line":316},[241,696,697],{}," except SyntaxError:\n",[241,699,700],{"class":243,"line":322},[241,701,702],{}," continue\n",[241,704,705],{"class":243,"line":328},[241,706,677],{},[241,708,709],{"class":243,"line":334},[241,710,711],{}," for node in ast.walk(tree):\n",[241,713,714],{"class":243,"line":339},[241,715,716],{}," if isinstance(node, ast.FunctionDef):\n",[241,718,719],{"class":243,"line":345},[241,720,721],{}," for dec in node.decorator_list:\n",[241,723,724],{"class":243,"line":351},[241,725,726],{}," # Handle @pytest.fixture and @pytest.fixture(name=\"...\")\n",[241,728,729],{"class":243,"line":357},[241,730,731],{}," if isinstance(dec, ast.Call) and hasattr(dec.func, \"attr\") and dec.func.attr == \"fixture\":\n",[241,733,734],{"class":243,"line":363},[241,735,736],{}," # Check for explicit name kwarg\n",[241,738,739],{"class":243,"line":369},[241,740,741],{}," name_kwarg = next((kw.value.value for kw in dec.keywords if kw.arg == \"name\"), None)\n",[241,743,744],{"class":243,"line":375},[241,745,746],{}," fixture_name = name_kwarg or node.name\n",[241,748,749],{"class":243,"line":381},[241,750,751],{}," fixtures.setdefault(fixture_name, []).append(str(conf.relative_to(root)))\n",[241,753,754],{"class":243,"line":387},[241,755,756],{}," elif isinstance(dec, ast.Name) and dec.id == \"fixture\":\n",[241,758,759],{"class":243,"line":393},[241,760,761],{}," fixtures.setdefault(node.name, []).append(str(conf.relative_to(root)))\n",[241,763,764],{"class":243,"line":399},[241,765,677],{},[241,767,769],{"class":243,"line":768},28,[241,770,771],{}," return {k: v for k, v in fixtures.items() if len(v) > 1}\n",[241,773,775],{"class":243,"line":774},29,[241,776,266],{"emptyLinePlaceholder":265},[241,778,780],{"class":243,"line":779},30,[241,781,782],{},"if __name__ == \"__main__\":\n",[241,784,786],{"class":243,"line":785},31,[241,787,788],{}," root = Path(sys.argv[1] if len(sys.argv) > 1 else \".\")\n",[241,790,792],{"class":243,"line":791},32,[241,793,794],{}," collisions = scan_conftest_fixtures(root)\n",[241,796,798],{"class":243,"line":797},33,[241,799,800],{}," if collisions:\n",[241,802,804],{"class":243,"line":803},34,[241,805,806],{}," print(\"️ Fixture collisions detected:\")\n",[241,808,810],{"class":243,"line":809},35,[241,811,812],{}," for name, paths in collisions.items():\n",[241,814,816],{"class":243,"line":815},36,[241,817,818],{}," print(f\" {name}: {', '.join(paths)}\")\n",[241,820,822],{"class":243,"line":821},37,[241,823,824],{}," sys.exit(1)\n",[241,826,828],{"class":243,"line":827},38,[241,829,830],{}," print(\"✅ No fixture collisions found.\")\n",[226,832,834],{"id":833},"pep-420-namespace-package-pitfalls","PEP 420 Namespace Package Pitfalls",[14,836,837,838,840,841,843,844,846],{},"Directories without ",[18,839,68],{}," are treated as implicit namespace packages. While beneficial for source code, they disrupt pytest's upward traversal. If a parent directory lacks ",[18,842,68],{},", pytest may skip ",[18,845,20],{}," files entirely, breaking fixture inheritance.",[14,848,849,852,853,855,856,858,859,80,862,864,865,868],{},[113,850,851],{},"Resolution",": Add empty ",[18,854,68],{}," to test directories containing ",[18,857,20],{},", or explicitly configure ",[18,860,861],{},"pythonpath = [\".\"]",[18,863,83],{},". Alternatively, use ",[18,866,867],{},"pytest --ignore=src"," to force explicit test path resolution and bypass namespace package ambiguity.",[26,870,872],{"id":871},"advanced-control-custom-hooks-collection-modification","Advanced Control: Custom Hooks & Collection Modification",[14,874,875,876,878,879,882],{},"For large-scale monorepos, static ",[18,877,20],{}," files often require runtime augmentation. Hooking into ",[18,880,881],{},"pytest_collection_modifyitems"," allows teams to filter, reorder, or inject markers without modifying test files.",[226,884,886],{"id":885},"dynamic-test-filtering-marker-injection","Dynamic Test Filtering & Marker Injection",[14,888,889,890,892,893,896],{},"The ",[18,891,881],{}," hook receives the complete list of collected ",[18,894,895],{},"Item"," objects before execution. This enables package-aware filtering, which is essential for CI pipelines running targeted test suites.",[216,898,900],{"className":235,"code":899,"language":237,"meta":224,"style":224},"# tests\u002Fconftest.py (advanced hook integration)\nimport pytest\n\ndef pytest_collection_modifyitems(config, items):\n \"\"\"Filter tests based on --package CLI option or inject markers.\"\"\"\n target_pkg = config.getoption(\"--package\", default=None)\n if not target_pkg:\n return\n \n skip_marker = pytest.mark.skip(reason=f\"Excluded: --package={target_pkg}\")\n for item in items:\n # Check if test belongs to target package path\n if target_pkg not in str(item.fspath):\n item.add_marker(skip_marker)\n",[18,901,902,907,911,915,920,925,930,935,940,944,949,954,959,964],{"__ignoreMap":224},[241,903,904],{"class":243,"line":244},[241,905,906],{},"# tests\u002Fconftest.py (advanced hook integration)\n",[241,908,909],{"class":243,"line":250},[241,910,253],{},[241,912,913],{"class":243,"line":256},[241,914,266],{"emptyLinePlaceholder":265},[241,916,917],{"class":243,"line":262},[241,918,919],{},"def pytest_collection_modifyitems(config, items):\n",[241,921,922],{"class":243,"line":269},[241,923,924],{}," \"\"\"Filter tests based on --package CLI option or inject markers.\"\"\"\n",[241,926,927],{"class":243,"line":275},[241,928,929],{}," target_pkg = config.getoption(\"--package\", default=None)\n",[241,931,932],{"class":243,"line":281},[241,933,934],{}," if not target_pkg:\n",[241,936,937],{"class":243,"line":286},[241,938,939],{}," return\n",[241,941,942],{"class":243,"line":292},[241,943,677],{},[241,945,946],{"class":243,"line":298},[241,947,948],{}," skip_marker = pytest.mark.skip(reason=f\"Excluded: --package={target_pkg}\")\n",[241,950,951],{"class":243,"line":304},[241,952,953],{}," for item in items:\n",[241,955,956],{"class":243,"line":310},[241,957,958],{}," # Check if test belongs to target package path\n",[241,960,961],{"class":243,"line":316},[241,962,963],{}," if target_pkg not in str(item.fspath):\n",[241,965,966],{"class":243,"line":322},[241,967,968],{}," item.add_marker(skip_marker)\n",[226,970,972,973],{"id":971},"early-plugin-bootstrapping-via-pytest_configure","Early Plugin Bootstrapping via ",[18,974,975],{},"pytest_configure",[14,977,889,978,980,981,983,984,986],{},[18,979,975],{}," hook executes before collection begins, making it ideal for dynamic ",[18,982,131],{}," assignment or environment validation. However, modifying ",[18,985,131],{}," at this stage requires caution to avoid hook recursion.",[216,988,990],{"className":235,"code":989,"language":237,"meta":224,"style":224},"def pytest_configure(config):\n \"\"\"Validate environment and conditionally register plugins.\"\"\"\n env = config.getoption(\"--env\")\n if env == \"staging\":\n # Dynamically register staging-specific fixtures\n config.pluginmanager.register(StagingNetworkMock(), \"staging_network\")\n \n # Prevent recursive hook execution\n if not hasattr(config, \"_hierarchy_validated\"):\n validate_hierarchy_integrity(config)\n config._hierarchy_validated = True\n",[18,991,992,997,1002,1007,1012,1017,1022,1026,1031,1036,1041],{"__ignoreMap":224},[241,993,994],{"class":243,"line":244},[241,995,996],{},"def pytest_configure(config):\n",[241,998,999],{"class":243,"line":250},[241,1000,1001],{}," \"\"\"Validate environment and conditionally register plugins.\"\"\"\n",[241,1003,1004],{"class":243,"line":256},[241,1005,1006],{}," env = config.getoption(\"--env\")\n",[241,1008,1009],{"class":243,"line":262},[241,1010,1011],{}," if env == \"staging\":\n",[241,1013,1014],{"class":243,"line":269},[241,1015,1016],{}," # Dynamically register staging-specific fixtures\n",[241,1018,1019],{"class":243,"line":275},[241,1020,1021],{}," config.pluginmanager.register(StagingNetworkMock(), \"staging_network\")\n",[241,1023,1024],{"class":243,"line":281},[241,1025,677],{},[241,1027,1028],{"class":243,"line":286},[241,1029,1030],{}," # Prevent recursive hook execution\n",[241,1032,1033],{"class":243,"line":292},[241,1034,1035],{}," if not hasattr(config, \"_hierarchy_validated\"):\n",[241,1037,1038],{"class":243,"line":298},[241,1039,1040],{}," validate_hierarchy_integrity(config)\n",[241,1042,1043],{"class":243,"line":304},[241,1044,1045],{}," config._hierarchy_validated = True\n",[226,1047,1049],{"id":1048},"avoiding-hook-recursion-stack-overflow","Avoiding Hook Recursion & Stack Overflow",[14,1051,1052,1053,1056,1057,1060,1061,1064],{},"Pytest's hook execution follows a strict chain. Modifying plugin state or calling ",[18,1054,1055],{},"config.pluginmanager.register()"," during collection can trigger infinite recursion if not guarded. Always use configuration flags (",[18,1058,1059],{},"config._hierarchy_validated",") to ensure hooks execute exactly once. Additionally, avoid calling ",[18,1062,1063],{},"pytest.main()"," or re-invoking collection inside hooks, as this resets the internal state machine and corrupts fixture caches.",[26,1066,1068],{"id":1067},"validation-profiling-ensuring-hierarchy-integrity","Validation & Profiling: Ensuring Hierarchy Integrity",[14,1070,1071,1072,1075],{},"Continuous validation prevents hierarchy drift. Integrate ",[18,1073,1074],{},"pytest --trace-config"," into CI pipelines to audit plugin loading sequences and enforce deterministic collection across environments.",[226,1077,1079],{"id":1078},"plugin-loading-audit","Plugin Loading Audit",[14,1081,1082,1084,1085,1087],{},[18,1083,1074],{}," outputs the exact order in which ",[18,1086,20],{}," files and plugins are registered. Scan this output for unexpected duplicates or out-of-order loading. In CI, pipe the output to a grep filter to assert that only expected plugins are active:",[216,1089,1093],{"className":1090,"code":1091,"language":1092,"meta":224,"style":224},"language-bash shiki shiki-themes github-light github-dark","pytest --trace-config 2>&1 | grep -E \"conftest|plugin\" > \u002Ftmp\u002Fplugin_audit.log\n# CI assertion: Verify no duplicate conftest paths\nif grep -q \"duplicate\" \u002Ftmp\u002Fplugin_audit.log; then\n echo \"❌ Plugin loading collision detected\"\n exit 1\nfi\n","bash",[18,1094,1095,1128,1134,1157,1165,1173],{"__ignoreMap":224},[241,1096,1097,1101,1105,1109,1112,1115,1118,1122,1125],{"class":243,"line":244},[241,1098,1100],{"class":1099},"sScJk","pytest",[241,1102,1104],{"class":1103},"sj4cs"," --trace-config",[241,1106,1108],{"class":1107},"szBVR"," 2>&1",[241,1110,1111],{"class":1107}," |",[241,1113,1114],{"class":1099}," grep",[241,1116,1117],{"class":1103}," -E",[241,1119,1121],{"class":1120},"sZZnC"," \"conftest|plugin\"",[241,1123,1124],{"class":1107}," >",[241,1126,1127],{"class":1120}," \u002Ftmp\u002Fplugin_audit.log\n",[241,1129,1130],{"class":243,"line":250},[241,1131,1133],{"class":1132},"sJ8bj","# CI assertion: Verify no duplicate conftest paths\n",[241,1135,1136,1139,1141,1144,1147,1150,1154],{"class":243,"line":256},[241,1137,1138],{"class":1107},"if",[241,1140,1114],{"class":1099},[241,1142,1143],{"class":1103}," -q",[241,1145,1146],{"class":1120}," \"duplicate\"",[241,1148,1149],{"class":1120}," \u002Ftmp\u002Fplugin_audit.log",[241,1151,1153],{"class":1152},"sVt8B","; ",[241,1155,1156],{"class":1107},"then\n",[241,1158,1159,1162],{"class":243,"line":262},[241,1160,1161],{"class":1103}," echo",[241,1163,1164],{"class":1120}," \"❌ Plugin loading collision detected\"\n",[241,1166,1167,1170],{"class":243,"line":269},[241,1168,1169],{"class":1103}," exit",[241,1171,1172],{"class":1103}," 1\n",[241,1174,1175],{"class":243,"line":275},[241,1176,1177],{"class":1107},"fi\n",[226,1179,1181],{"id":1180},"fixture-overhead-profiling","Fixture Overhead Profiling",[14,1183,615,1184,1187,1188,1190,1191,1194],{},[18,1185,1186],{},"pytest --durations=10"," to identify the slowest fixtures. Combine with ",[18,1189,1074],{}," to verify ",[18,1192,1193],{},"conftest"," loading order. For granular measurement, wrap session-scoped fixtures with timing decorators:",[216,1196,1198],{"className":235,"code":1197,"language":237,"meta":224,"style":224},"import time\nimport pytest\nfrom functools import wraps\n\ndef profiled_fixture(func):\n @wraps(func)\n def wrapper(*args, **kwargs):\n start = time.perf_counter()\n result = yield from func(*args, **kwargs)\n duration = time.perf_counter() - start\n if duration > 2.0:\n pytest._log.warning(f\"Fixture {func.__name__} took {duration:.2f}s\")\n return result\n return wrapper\n",[18,1199,1200,1205,1209,1214,1218,1223,1228,1233,1238,1243,1248,1253,1258,1263],{"__ignoreMap":224},[241,1201,1202],{"class":243,"line":244},[241,1203,1204],{},"import time\n",[241,1206,1207],{"class":243,"line":250},[241,1208,253],{},[241,1210,1211],{"class":243,"line":256},[241,1212,1213],{},"from functools import wraps\n",[241,1215,1216],{"class":243,"line":262},[241,1217,266],{"emptyLinePlaceholder":265},[241,1219,1220],{"class":243,"line":269},[241,1221,1222],{},"def profiled_fixture(func):\n",[241,1224,1225],{"class":243,"line":275},[241,1226,1227],{}," @wraps(func)\n",[241,1229,1230],{"class":243,"line":281},[241,1231,1232],{}," def wrapper(*args, **kwargs):\n",[241,1234,1235],{"class":243,"line":286},[241,1236,1237],{}," start = time.perf_counter()\n",[241,1239,1240],{"class":243,"line":292},[241,1241,1242],{}," result = yield from func(*args, **kwargs)\n",[241,1244,1245],{"class":243,"line":298},[241,1246,1247],{}," duration = time.perf_counter() - start\n",[241,1249,1250],{"class":243,"line":304},[241,1251,1252],{}," if duration > 2.0:\n",[241,1254,1255],{"class":243,"line":310},[241,1256,1257],{}," pytest._log.warning(f\"Fixture {func.__name__} took {duration:.2f}s\")\n",[241,1259,1260],{"class":243,"line":316},[241,1261,1262],{}," return result\n",[241,1264,1265],{"class":243,"line":322},[241,1266,1267],{}," return wrapper\n",[226,1269,1271],{"id":1270},"ci-pipeline-validation-scripts","CI Pipeline Validation Scripts",[14,1273,1274],{},"Automate hierarchy checks using pre-commit hooks and CI gates:",[107,1276,1277,1286,1296],{},[110,1278,1279,1282,1283,1285],{},[113,1280,1281],{},"Pre-commit",": Run the AST collision detector and enforce ",[18,1284,79],{}," configuration.",[110,1287,1288,1291,1292,1295],{},[113,1289,1290],{},"CI Gate",": Execute ",[18,1293,1294],{},"pytest --collect-only --strict-markers"," to verify marker consistency.",[110,1297,1298,1301,1302,1305],{},[113,1299,1300],{},"Profiling Gate",": Fail builds if ",[18,1303,1304],{},"--durations"," output exceeds baseline thresholds by >15%.",[14,1307,1308,1309,1311],{},"These automated checks ensure that ",[18,1310,20],{}," best practices python developers rely on remain enforced as the monorepo scales.",[1313,1314],"hr",{},[26,1316,1318],{"id":1317},"common-pitfalls-resolution-matrix","Common Pitfalls & Resolution Matrix",[1320,1321,1322,1340],"table",{},[1323,1324,1325],"thead",{},[1326,1327,1328,1332,1335,1338],"tr",{},[1329,1330,1331],"th",{},"Pitfall",[1329,1333,1334],{},"Symptom",[1329,1336,1337],{},"Diagnosis",[1329,1339,851],{},[1341,1342,1343,1368,1408,1440],"tbody",{},[1326,1344,1345,1351,1354,1362],{},[1346,1347,1348],"td",{},[113,1349,1350],{},"Fixture Name Shadowing",[1346,1352,1353],{},"Tests in package B unexpectedly use package A's fixture implementation.",[1346,1355,1356,1358,1359,1361],{},[18,1357,571],{}," shows identical names in sibling ",[18,1360,20],{}," files.",[1346,1363,1364,1365,1367],{},"Prefix fixtures with domain identifiers or use ",[18,1366,618],{},". Implement CI linter.",[1326,1369,1370,1377,1380,1392],{},[1346,1371,1372],{},[113,1373,1374,1375],{},"conftest.py Inside ",[18,1376,522],{},[1346,1378,1379],{},"Pytest collects production code; fixtures leak into runtime imports.",[1346,1381,1382,1383,80,1385,1387,1388,1391],{},"Check ",[18,1384,79],{},[18,1386,83],{},". Run ",[18,1389,1390],{},"pytest --collect-only"," to verify tree.",[1346,1393,1394,1395,1397,1398,1400,1401,1404,1405,1407],{},"Move all ",[18,1396,20],{}," to ",[18,1399,529],{},". Use ",[18,1402,1403],{},"norecursedirs = src",". Remove ",[18,1406,68],{}," from test dirs.",[1326,1409,1410,1417,1424,1430],{},[1346,1411,1412],{},[113,1413,1414,1416],{},[18,1415,131],{}," Misconfiguration",[1346,1418,1419,1420,1423],{},"Plugins fail silently or raise ",[18,1421,1422],{},"ModuleNotFoundError",".",[1346,1425,1426,1427,1429],{},"Enable ",[18,1428,1074],{},". Check for string concatenation instead of list assignment.",[1346,1431,1432,1433,1436,1437,1423],{},"Always use ",[18,1434,1435],{},"pytest_plugins = [\"pkg.module\"]",". Validate paths with ",[18,1438,1439],{},"importlib.util.find_spec()",[1326,1441,1442,1447,1455,1462],{},[1346,1443,1444],{},[113,1445,1446],{},"Namespace Package Discovery Failure",[1346,1448,1449,1450,1452,1453,1423],{},"Pytest skips ",[18,1451,20],{}," in directories lacking ",[18,1454,68],{},[1346,1456,1457,1458,1461],{},"Run ",[18,1459,1460],{},"python -c \"import sys; print(sys.path)\"",". Verify pytest's collection root.",[1346,1463,1464,1465,1467,1468,80,1471,1423],{},"Add ",[18,1466,68],{}," to test directories or configure ",[18,1469,1470],{},"pythonpath",[18,1472,83],{},[1313,1474],{},[26,1476,1478],{"id":1477},"frequently-asked-questions","Frequently Asked Questions",[14,1480,1481,1484,1485,1488,1489,1491,1492,1494,1495,1497],{},[113,1482,1483],{},"Can I safely share fixtures across completely unrelated monorepo packages?","\nYes, but only through a centralized ",[18,1486,1487],{},"tests\u002Fshared\u002F"," directory referenced via ",[18,1490,131],{}," in each package's ",[18,1493,20],{},". Avoid placing shared fixtures in the root ",[18,1496,20],{}," unless they are truly global. Use explicit imports or plugin registration to maintain traceability and prevent implicit scope leakage.",[14,1499,1500,1506,1507,1509],{},[113,1501,1502,1503,1505],{},"Why does pytest ignore my root ",[18,1504,20],{}," when running tests from a subdirectory?","\nPytest only loads ",[18,1508,20],{}," files that are ancestors of the collected test files. If you run `pytest services\u002Fauth\u002Ftests",[1511,1512,1513],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":224,"searchDepth":250,"depth":250,"links":1515},[1516,1517,1518,1522,1527,1533,1538,1539],{"id":28,"depth":250,"text":29},{"id":93,"depth":250,"text":94},{"id":207,"depth":250,"text":208,"children":1519},[1520,1521],{"id":228,"depth":256,"text":229},{"id":405,"depth":256,"text":406},{"id":564,"depth":250,"text":565,"children":1523},[1524,1525,1526],{"id":575,"depth":256,"text":576},{"id":625,"depth":256,"text":626},{"id":833,"depth":256,"text":834},{"id":871,"depth":250,"text":872,"children":1528},[1529,1530,1532],{"id":885,"depth":256,"text":886},{"id":971,"depth":256,"text":1531},"Early Plugin Bootstrapping via pytest_configure",{"id":1048,"depth":256,"text":1049},{"id":1067,"depth":250,"text":1068,"children":1534},[1535,1536,1537],{"id":1078,"depth":256,"text":1079},{"id":1180,"depth":256,"text":1181},{"id":1270,"depth":256,"text":1271},{"id":1317,"depth":250,"text":1318},{"id":1477,"depth":250,"text":1478},"Scaling pytest across a multi-package monorepo introduces architectural complexity that flat test configurations cannot sustain. As test suites grow, implicit fixture resolution, unpredictable plugin loading, and collection bloat frequently degrade CI reliability and developer velocity. Establishing a deterministic conftest.py hierarchy is not merely an organizational preference; it is a foundational requirement for maintaining strict scope boundaries, preventing cross-package leakage, and enabling parallelized execution. This guide details production-grade strategies for architecting, debugging, and validating conftest.py hierarchies in modern Python monorepos.","md",{},"\u002Fadvanced-pytest-architecture-configuration\u002Fmanaging-conftest-hierarchies\u002Fcreating-conftestpy-hierarchies-for-monorepos",{"title":5,"description":1540},"advanced-pytest-architecture-configuration\u002Fmanaging-conftest-hierarchies\u002Fcreating-conftestpy-hierarchies-for-monorepos\u002Findex","wGz6nTh3FMjy1vmkA1QxUxpXB03fSUM4sK6sBE6s450",1778004579233]