MAINT: Remove sys.path mutations from build_scripts - #2368
Open
Roman Lutz (romanlutz) wants to merge 2 commits into
Open
MAINT: Remove sys.path mutations from build_scripts#2368Roman Lutz (romanlutz) wants to merge 2 commits into
Roman Lutz (romanlutz) wants to merge 2 commits into
Conversation
build_scripts/ was not a package, so sibling modules were imported via `sys.path.insert(0, <script dir>)` (gen_api_md.py, example_index.py) or by bare name with no guard at all (compose_docs_dist.py importing generate_pages_manifest). Two problems. First, mixing `import validate_docs` with `from build_scripts import validate_docs` creates two distinct module objects for the same file. Second, inserting the script directory at sys.path[0] pollutes the path for the whole process — under pytest that means the entire test session, where a future build_scripts/types.py or build_scripts/json.py would silently shadow a stdlib module. Make build_scripts a real package and import siblings as `from build_scripts import <module>`. Package-qualified imports do not resolve under direct script execution (the script's own directory is sys.path[0], not the repo root), so every invocation moves to `python -m build_scripts.<name>`: Makefile, pre-commit hooks, the docs workflow, the Dockerfile, and the usage examples in module docstrings and contributing docs. The docs workflow checks out individual build_scripts files with sparse-checkout in cone-mode-false, so __init__.py is added to both sparse-checkout blocks; gen_api_md.py's new dependencies are also added to the workflow's path triggers. build_scripts stays out of the distribution — packages.find only includes pyrit/pyrit.*, confirmed against a built sdist. Adds an AST-based guard test that rejects any future sys.path mutation or bare sibling import in build_scripts/. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a8b8c380-b64e-4ea7-a731-fb8c3b1778ab
Enumerating individual build_scripts files in the paths trigger and the sparse-checkout blocks is fragile: adding a sibling import means remembering to list the new file, and forgetting it breaks CI at runtime with ModuleNotFoundError. Use build_scripts/** for the path trigger and an anchored /build_scripts/ for both sparse checkouts. The leading slash matters — non-cone sparse-checkout patterns are gitignore-style, so a bare 'build_scripts' also matches tests/unit/build_scripts/. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a8b8c380-b64e-4ea7-a731-fb8c3b1778ab
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Scripts in
build_scripts/reached their siblings in three different ways, none of them sound.gen_api_md.pyandexample_index.pydidsys.path.insert(0, <script dir>)followed by a bareimport validate_docs, andcompose_docs_dist.pyimportedgenerate_pages_manifestby bare name with no guard at all, working purely by accident of direct execution.Two concrete problems. First, mixing
import validate_docswithfrom build_scripts import validate_docscreates two distinct module objects for the same file, which is why #2282 needed a regression test to pin it down. Second, inserting the script directory atsys.path[0]pollutes the path for the entire process. Under pytest that means the whole test session, so a futurebuild_scripts/types.pyorbuild_scripts/json.pywould silently shadow a stdlib module for every test in the run.This makes
build_scripts/a real package and imports siblings asfrom build_scripts import <module>, removing thesys.pathmutations entirely.Invocation changes, and why it was unavoidable. Package-qualified imports do not resolve under direct script execution: with
python build_scripts/gen_api_md.py, the script's own directory issys.path[0]and the repo root is not on the path at all, so the import raisesModuleNotFoundError. I verified this empirically rather than assuming it. So every call site moves topython -m build_scripts.<name>: the Makefile, the pre-commit hooks, the docs workflow, the Dockerfile, and the usage examples in module docstrings and contributing docs. The upside is one simple rule, always-mfrom the repo root, which also stops a future contributor from reintroducing this by adding a sibling import to a path-invoked script.Worth flagging for contributors:
python build_scripts/evaluate_scorers.pyand friends no longer work; usepython -m build_scripts.evaluate_scorers. This is a developer workflow change only.build_scriptsis not part of the distributed package (packages.findonly includespyritandpyrit.*), so nothing changes for people who install PyRIT, and I confirmed against a built sdist thatbuild_scriptsis absent from it.One non-obvious detail in the docs workflow. It uses
sparse-checkoutwithsparse-checkout-cone-mode: false, which makes the patterns gitignore-style. Those blocks previously enumerated individual script files, which is fragile now that these scripts import each other: a missed entry breaks CI at runtime withModuleNotFoundError. They now match the directory wholesale as/build_scripts/. The leading slash matters. A barebuild_scriptshas no separator, so under gitignore rules it matches at any depth and also drags intests/unit/build_scripts/. Thepaths:trigger is likewise collapsed tobuild_scripts/**. That does mean the docs build now triggers on unrelatedbuild_scripts/changes, but the workflow already triggers on all ofpyrit/**anddoc/**, and an occasional extra run is much cheaper than a silently missed one.Tests and Documentation
Added
tests/unit/build_scripts/test_import_hygiene.py, an AST-based guard that scans everybuild_scripts/*.pyand fails on anysys.pathmutation or any bare top-level import of a sibling module. Source-based rather than runtimesys.pathinspection, which would be order-dependent and flaky. I confirmed it is not vacuous by injecting asys.path.insertplus a bare sibling import intovalidate_docs.pyand watching both guards fire.Also de-hacked two existing tests that were doing the same thing:
test_compose_docs_dist.pyreplaced itssys.path.insertplusspec_from_file_locationfixture with a plain import, andtests/unit/memory/test_migration.pydropped itssys.path.insert. Thetest_docs_scripts_share_validate_docs_moduleregression test from #2282 still passes unchanged.Verification performed:
pydoc2jsonthengen_api_md. PrintsIndexed examples: 236 symbols across 74 pagesand[OK] All documentation validations passed!, with a cleangit diffondoc/api/, so generated output is byte identical to before.pre-commit run --all-filespasses, which exercises all 8 converted hooks.versionsanddeployjobs against it in a clean venv with only PyYAML installed and no editable install, mirroring CI.resolve_docs_matrix,compose_docs_dist, andinject_version_pickerall succeed.sys.pathentries are added on import, that all modules resolve to a single shared object, and thatbuild_scriptsis absent from a built sdist.Documentation: updated the usage examples in
doc/contributing/10_release_process.md,doc/contributing/11_memory_models.md, and thedoc/code/scoring/4_scorer_metricsjupytext pair. The notebook hits are all inside markdown cells, so the.pyand.ipynbhalves received identical textual edits and no re-execution was needed. I verified the pair stays in sync by round-tripping the.ipynbthrough jupytext and diffing against the.py.