didibreak 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- didibreak-0.1.0/.gitignore +28 -0
- didibreak-0.1.0/LICENSE +21 -0
- didibreak-0.1.0/PKG-INFO +45 -0
- didibreak-0.1.0/README.md +17 -0
- didibreak-0.1.0/hatch_build.py +58 -0
- didibreak-0.1.0/pyproject.toml +100 -0
- didibreak-0.1.0/scripts/check_dist.py +103 -0
- didibreak-0.1.0/scripts/evaluate.py +386 -0
- didibreak-0.1.0/scripts/llm_smoke.py +111 -0
- didibreak-0.1.0/scripts/package_smoke.py +179 -0
- didibreak-0.1.0/scripts/sync_prompts.py +114 -0
- didibreak-0.1.0/src/didibreak/__init__.py +3 -0
- didibreak-0.1.0/src/didibreak/__main__.py +5 -0
- didibreak-0.1.0/src/didibreak/_harness_child.py +280 -0
- didibreak-0.1.0/src/didibreak/cache.py +156 -0
- didibreak-0.1.0/src/didibreak/cases.py +326 -0
- didibreak-0.1.0/src/didibreak/cli.py +707 -0
- didibreak-0.1.0/src/didibreak/compare.py +276 -0
- didibreak-0.1.0/src/didibreak/config.py +371 -0
- didibreak-0.1.0/src/didibreak/contracts.py +227 -0
- didibreak-0.1.0/src/didibreak/functions.py +367 -0
- didibreak-0.1.0/src/didibreak/gitdiff.py +438 -0
- didibreak-0.1.0/src/didibreak/harness.py +373 -0
- didibreak-0.1.0/src/didibreak/intent.py +163 -0
- didibreak-0.1.0/src/didibreak/llm/__init__.py +122 -0
- didibreak-0.1.0/src/didibreak/llm/anthropic.py +175 -0
- didibreak-0.1.0/src/didibreak/llm/common.py +406 -0
- didibreak-0.1.0/src/didibreak/llm/openai_compat.py +218 -0
- didibreak-0.1.0/src/didibreak/llm/pricing.py +169 -0
- didibreak-0.1.0/src/didibreak/llm_session.py +292 -0
- didibreak-0.1.0/src/didibreak/pipeline.py +594 -0
- didibreak-0.1.0/src/didibreak/prompts/case_generation.md +31 -0
- didibreak-0.1.0/src/didibreak/prompts/case_refinement.md +34 -0
- didibreak-0.1.0/src/didibreak/prompts/intent_labelling.md +21 -0
- didibreak-0.1.0/src/didibreak/report/__init__.py +98 -0
- didibreak-0.1.0/src/didibreak/report/json.py +82 -0
- didibreak-0.1.0/src/didibreak/report/pytest_writer.py +357 -0
- didibreak-0.1.0/src/didibreak/report/text.py +156 -0
- didibreak-0.1.0/src/didibreak/testability.py +382 -0
- didibreak-0.1.0/tests/conftest.py +63 -0
- didibreak-0.1.0/tests/fixture_repos.py +112 -0
- didibreak-0.1.0/tests/fixtures/exception_type/after/app/parsing.py +5 -0
- didibreak-0.1.0/tests/fixtures/exception_type/before/app/parsing.py +7 -0
- didibreak-0.1.0/tests/fixtures/exception_type/expected.json +48 -0
- didibreak-0.1.0/tests/fixtures/float_precision/after/app/stats.py +11 -0
- didibreak-0.1.0/tests/fixtures/float_precision/before/app/stats.py +9 -0
- didibreak-0.1.0/tests/fixtures/float_precision/expected.json +45 -0
- didibreak-0.1.0/tests/fixtures/formatting_only/after/app/limits.py +10 -0
- didibreak-0.1.0/tests/fixtures/formatting_only/before/app/limits.py +5 -0
- didibreak-0.1.0/tests/fixtures/formatting_only/expected.json +19 -0
- didibreak-0.1.0/tests/fixtures/intended_change/after/app/greetings.py +5 -0
- didibreak-0.1.0/tests/fixtures/intended_change/before/app/greetings.py +5 -0
- didibreak-0.1.0/tests/fixtures/intended_change/expected.json +33 -0
- didibreak-0.1.0/tests/fixtures/new_function/after/app/text.py +14 -0
- didibreak-0.1.0/tests/fixtures/new_function/before/app/text.py +5 -0
- didibreak-0.1.0/tests/fixtures/new_function/expected.json +41 -0
- didibreak-0.1.0/tests/fixtures/new_function_validation/after/app/percent.py +24 -0
- didibreak-0.1.0/tests/fixtures/new_function_validation/before/app/percent.py +5 -0
- didibreak-0.1.0/tests/fixtures/new_function_validation/expected.json +19 -0
- didibreak-0.1.0/tests/fixtures/nondeterministic/after/app/tokens.py +7 -0
- didibreak-0.1.0/tests/fixtures/nondeterministic/before/app/tokens.py +7 -0
- didibreak-0.1.0/tests/fixtures/nondeterministic/expected.json +22 -0
- didibreak-0.1.0/tests/fixtures/phone_regression/after/app/utils/phone.py +9 -0
- didibreak-0.1.0/tests/fixtures/phone_regression/before/app/utils/phone.py +9 -0
- didibreak-0.1.0/tests/fixtures/phone_regression/expected.json +30 -0
- didibreak-0.1.0/tests/llm_responses/malicious_cases.json +16 -0
- didibreak-0.1.0/tests/llm_responses/openai_phone_cases.json +25 -0
- didibreak-0.1.0/tests/report_samples.py +95 -0
- didibreak-0.1.0/tests/snapshots/report.json +109 -0
- didibreak-0.1.0/tests/snapshots/report.txt +18 -0
- didibreak-0.1.0/tests/test_ai_agents_doc.py +52 -0
- didibreak-0.1.0/tests/test_base_worktree.py +153 -0
- didibreak-0.1.0/tests/test_case_cache.py +101 -0
- didibreak-0.1.0/tests/test_case_generation.py +309 -0
- didibreak-0.1.0/tests/test_case_prompt.py +187 -0
- didibreak-0.1.0/tests/test_case_validation.py +125 -0
- didibreak-0.1.0/tests/test_cases_file.py +67 -0
- didibreak-0.1.0/tests/test_changed_files.py +171 -0
- didibreak-0.1.0/tests/test_check_command.py +400 -0
- didibreak-0.1.0/tests/test_check_cost_and_cache.py +265 -0
- didibreak-0.1.0/tests/test_check_dist.py +105 -0
- didibreak-0.1.0/tests/test_check_end_to_end.py +191 -0
- didibreak-0.1.0/tests/test_check_labelling.py +226 -0
- didibreak-0.1.0/tests/test_check_llm_end_to_end.py +199 -0
- didibreak-0.1.0/tests/test_check_llm_settings.py +181 -0
- didibreak-0.1.0/tests/test_check_refinement.py +162 -0
- didibreak-0.1.0/tests/test_check_reports.py +152 -0
- didibreak-0.1.0/tests/test_check_reproducing_tests.py +118 -0
- didibreak-0.1.0/tests/test_cli.py +67 -0
- didibreak-0.1.0/tests/test_compare_fixtures.py +123 -0
- didibreak-0.1.0/tests/test_compare_rules.py +191 -0
- didibreak-0.1.0/tests/test_compare_values.py +83 -0
- didibreak-0.1.0/tests/test_config.py +178 -0
- didibreak-0.1.0/tests/test_contracts.py +114 -0
- didibreak-0.1.0/tests/test_definitions.py +115 -0
- didibreak-0.1.0/tests/test_docs_anthropic_label.py +25 -0
- didibreak-0.1.0/tests/test_docs_coverage.py +49 -0
- didibreak-0.1.0/tests/test_docs_precision_claim.py +42 -0
- didibreak-0.1.0/tests/test_evaluate_script.py +193 -0
- didibreak-0.1.0/tests/test_exclusions.py +69 -0
- didibreak-0.1.0/tests/test_explicit_raises.py +96 -0
- didibreak-0.1.0/tests/test_finding_id.py +56 -0
- didibreak-0.1.0/tests/test_fixture_repos.py +143 -0
- didibreak-0.1.0/tests/test_functions.py +262 -0
- didibreak-0.1.0/tests/test_gitdiff.py +86 -0
- didibreak-0.1.0/tests/test_harness.py +360 -0
- didibreak-0.1.0/tests/test_harness_outcomes.py +207 -0
- didibreak-0.1.0/tests/test_how_it_works_doc.py +27 -0
- didibreak-0.1.0/tests/test_import_roots.py +80 -0
- didibreak-0.1.0/tests/test_init_agents.py +68 -0
- didibreak-0.1.0/tests/test_intent_labels.py +138 -0
- didibreak-0.1.0/tests/test_intent_text.py +69 -0
- didibreak-0.1.0/tests/test_limitations_doc.py +27 -0
- didibreak-0.1.0/tests/test_llm_anthropic.py +167 -0
- didibreak-0.1.0/tests/test_llm_common.py +289 -0
- didibreak-0.1.0/tests/test_llm_factory.py +141 -0
- didibreak-0.1.0/tests/test_llm_openai_compat.py +215 -0
- didibreak-0.1.0/tests/test_llm_smoke.py +88 -0
- didibreak-0.1.0/tests/test_output_format_doc.py +57 -0
- didibreak-0.1.0/tests/test_package_smoke.py +67 -0
- didibreak-0.1.0/tests/test_pipeline.py +175 -0
- didibreak-0.1.0/tests/test_prices_doc.py +37 -0
- didibreak-0.1.0/tests/test_pricing.py +135 -0
- didibreak-0.1.0/tests/test_pytest_writer.py +340 -0
- didibreak-0.1.0/tests/test_release_workflow.py +54 -0
- didibreak-0.1.0/tests/test_report_json.py +86 -0
- didibreak-0.1.0/tests/test_report_schema.py +58 -0
- didibreak-0.1.0/tests/test_report_text.py +61 -0
- didibreak-0.1.0/tests/test_safety_doc.py +24 -0
- didibreak-0.1.0/tests/test_sync_prompts.py +100 -0
- didibreak-0.1.0/tests/test_testability.py +318 -0
- didibreak-0.1.0/tests/test_working_tree_copy.py +66 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Python bytecode and caches
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.mypy_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
|
|
8
|
+
# Virtual environments
|
|
9
|
+
.venv/
|
|
10
|
+
|
|
11
|
+
# Build output
|
|
12
|
+
build/
|
|
13
|
+
dist/
|
|
14
|
+
*.egg-info/
|
|
15
|
+
|
|
16
|
+
# Generated at build time from spec/prompts/ (python/scripts/sync_prompts.py)
|
|
17
|
+
python/src/didibreak/prompts/
|
|
18
|
+
|
|
19
|
+
# Copied from the root LICENSE at build time (python/hatch_build.py)
|
|
20
|
+
python/LICENSE
|
|
21
|
+
|
|
22
|
+
# didibreak output when run on this repository
|
|
23
|
+
.didibreak/
|
|
24
|
+
|
|
25
|
+
# OS and editor files
|
|
26
|
+
.DS_Store
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
didibreak-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Levan Changelia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
didibreak-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: didibreak
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Find unintended behavior changes in Python code by running the old and new versions side by side.
|
|
5
|
+
Project-URL: Homepage, https://github.com/changelia/didibreak
|
|
6
|
+
Project-URL: Documentation, https://github.com/changelia/didibreak/tree/main/docs
|
|
7
|
+
Project-URL: Changelog, https://github.com/changelia/didibreak/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/changelia/didibreak/issues
|
|
9
|
+
Author: changelia
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai agents,behavior change,differential testing,llm,regression,testing
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
|
+
Classifier: Topic :: Software Development :: Testing
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: httpx<1,>=0.28
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# didibreak (Python)
|
|
30
|
+
|
|
31
|
+
didibreak finds unintended behavior changes in Python code changes. It runs the
|
|
32
|
+
old and new versions of each changed function on the same generated inputs and
|
|
33
|
+
reports where the results differ.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install didibreak
|
|
37
|
+
export OPENAI_API_KEY="sk-..." # or ANTHROPIC_API_KEY (experimental, not live-verified)
|
|
38
|
+
didibreak check --intent "what the change should do"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python 3.11 or newer, Linux or macOS, and git. Version 0.1.0 is the
|
|
42
|
+
first release (alpha). No telemetry: the only network requests go to the LLM
|
|
43
|
+
provider you configure, with your own API key.
|
|
44
|
+
|
|
45
|
+
Full documentation: https://github.com/changelia/didibreak/tree/main/docs
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# didibreak (Python)
|
|
2
|
+
|
|
3
|
+
didibreak finds unintended behavior changes in Python code changes. It runs the
|
|
4
|
+
old and new versions of each changed function on the same generated inputs and
|
|
5
|
+
reports where the results differ.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install didibreak
|
|
9
|
+
export OPENAI_API_KEY="sk-..." # or ANTHROPIC_API_KEY (experimental, not live-verified)
|
|
10
|
+
didibreak check --intent "what the change should do"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Python 3.11 or newer, Linux or macOS, and git. Version 0.1.0 is the
|
|
14
|
+
first release (alpha). No telemetry: the only network requests go to the LLM
|
|
15
|
+
provider you configure, with your own API key.
|
|
16
|
+
|
|
17
|
+
Full documentation: https://github.com/changelia/didibreak/tree/main/docs
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Hatch hooks that copy shared repository files into the Python project.
|
|
2
|
+
|
|
3
|
+
- The metadata hook copies the root LICENSE to python/LICENSE. It must run in
|
|
4
|
+
the metadata phase: Hatch resolves ``license-files`` when it builds the
|
|
5
|
+
metadata, before any build hook runs.
|
|
6
|
+
- The build hook copies spec/prompts/ into src/didibreak/prompts/ before each
|
|
7
|
+
sdist, wheel and editable build.
|
|
8
|
+
|
|
9
|
+
When building from an extracted sdist there is no repository around the
|
|
10
|
+
project; the sdist already contains both copies, so the hooks leave them as
|
|
11
|
+
they are.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import importlib.util
|
|
15
|
+
import shutil
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
21
|
+
from hatchling.metadata.plugin.interface import MetadataHookInterface
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _load_sync_prompts(project_root: Path) -> ModuleType:
|
|
25
|
+
script = project_root / "scripts" / "sync_prompts.py"
|
|
26
|
+
module_spec = importlib.util.spec_from_file_location("sync_prompts", script)
|
|
27
|
+
if module_spec is None or module_spec.loader is None:
|
|
28
|
+
raise RuntimeError(f"Cannot load {script}")
|
|
29
|
+
module = importlib.util.module_from_spec(module_spec)
|
|
30
|
+
module_spec.loader.exec_module(module)
|
|
31
|
+
return module
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LicenseCopyMetadataHook(MetadataHookInterface):
|
|
35
|
+
"""Copy the repository LICENSE so ``license-files`` can find it."""
|
|
36
|
+
|
|
37
|
+
PLUGIN_NAME = "custom"
|
|
38
|
+
|
|
39
|
+
def update(self, metadata: dict[str, Any]) -> None:
|
|
40
|
+
"""Copy the license; the metadata itself is left unchanged."""
|
|
41
|
+
project_root = Path(self.root)
|
|
42
|
+
repository_license = project_root.parent / "LICENSE"
|
|
43
|
+
if repository_license.is_file():
|
|
44
|
+
shutil.copyfile(repository_license, project_root / "LICENSE")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PromptCopyBuildHook(BuildHookInterface):
|
|
48
|
+
"""Copy the shared prompt templates into src/didibreak/prompts/."""
|
|
49
|
+
|
|
50
|
+
PLUGIN_NAME = "custom"
|
|
51
|
+
|
|
52
|
+
def initialize(self, version: str, build_data: dict[str, Any]) -> None:
|
|
53
|
+
"""Copy the prompts before Hatch collects the files to build."""
|
|
54
|
+
sync_prompts = _load_sync_prompts(Path(self.root))
|
|
55
|
+
if sync_prompts.SPEC_PROMPTS.is_dir():
|
|
56
|
+
sync_prompts.copy_prompts(
|
|
57
|
+
sync_prompts.SPEC_PROMPTS, sync_prompts.PACKAGE_PROMPTS
|
|
58
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "didibreak"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Find unintended behavior changes in Python code by running the old and new versions side by side."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "changelia" }]
|
|
14
|
+
keywords = ["behavior change", "regression", "testing", "llm", "ai agents", "differential testing"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: MacOS",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
27
|
+
"Topic :: Software Development :: Testing",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"httpx>=0.28,<1",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/changelia/didibreak"
|
|
35
|
+
Documentation = "https://github.com/changelia/didibreak/tree/main/docs"
|
|
36
|
+
Changelog = "https://github.com/changelia/didibreak/blob/main/CHANGELOG.md"
|
|
37
|
+
Issues = "https://github.com/changelia/didibreak/issues"
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
didibreak = "didibreak.cli:main"
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest",
|
|
45
|
+
"ruff",
|
|
46
|
+
"mypy",
|
|
47
|
+
"jsonschema==4.26.0", # dev only: validates reports against spec/report.schema.json
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
target-version = "py311"
|
|
52
|
+
# Fixture repositories hold sample user code, some of it deliberately
|
|
53
|
+
# unformatted; they are data, not project code.
|
|
54
|
+
extend-exclude = ["tests/fixtures"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
# Rule set from the Code quality standards.
|
|
58
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF", "D"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint.pydocstyle]
|
|
61
|
+
convention = "google"
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint.per-file-ignores]
|
|
64
|
+
# Test names describe the behavior; docstrings on each test add nothing.
|
|
65
|
+
"tests/**" = ["D1"]
|
|
66
|
+
|
|
67
|
+
[tool.mypy]
|
|
68
|
+
strict = true
|
|
69
|
+
python_version = "3.11"
|
|
70
|
+
|
|
71
|
+
[tool.pytest.ini_options]
|
|
72
|
+
testpaths = ["tests"]
|
|
73
|
+
addopts = ["-ra", "--strict-markers", "--strict-config"]
|
|
74
|
+
xfail_strict = true
|
|
75
|
+
|
|
76
|
+
[tool.hatch.version]
|
|
77
|
+
path = "src/didibreak/__init__.py"
|
|
78
|
+
|
|
79
|
+
[tool.hatch.build]
|
|
80
|
+
# Copied from the repository at build time (git-ignored) but must be packaged.
|
|
81
|
+
artifacts = ["src/didibreak/prompts/", "/LICENSE"]
|
|
82
|
+
|
|
83
|
+
[tool.hatch.metadata.hooks.custom]
|
|
84
|
+
# hatch_build.py: copies the root LICENSE to python/LICENSE.
|
|
85
|
+
|
|
86
|
+
[tool.hatch.build.hooks.custom]
|
|
87
|
+
# hatch_build.py: copies spec/prompts/ into src/didibreak/prompts/.
|
|
88
|
+
|
|
89
|
+
[tool.hatch.build.targets.wheel]
|
|
90
|
+
packages = ["src/didibreak"]
|
|
91
|
+
|
|
92
|
+
[tool.hatch.build.targets.sdist]
|
|
93
|
+
include = [
|
|
94
|
+
"src/didibreak",
|
|
95
|
+
"tests",
|
|
96
|
+
"scripts",
|
|
97
|
+
"hatch_build.py",
|
|
98
|
+
"README.md",
|
|
99
|
+
"LICENSE",
|
|
100
|
+
]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Check the contents of the built wheel and sdist in a dist/ folder.
|
|
2
|
+
|
|
3
|
+
The packages must contain only the Python package (no spec/, docs/ or JS
|
|
4
|
+
files, except the copied prompts) and must ship the license. CI runs this
|
|
5
|
+
after ``uv build``; it uses only the standard library.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
import tarfile
|
|
11
|
+
import zipfile
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_FORBIDDEN_SDIST_FOLDERS = ("spec/", "docs/", "js/")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def check_wheel(wheel: Path) -> list[str]:
|
|
19
|
+
"""List problems in a wheel; empty when it is correct.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
wheel: Path to the ``.whl`` file.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
One human-readable line per problem.
|
|
26
|
+
"""
|
|
27
|
+
with zipfile.ZipFile(wheel) as archive:
|
|
28
|
+
names = archive.namelist()
|
|
29
|
+
problems = [
|
|
30
|
+
f"unexpected file in wheel: {name}"
|
|
31
|
+
for name in names
|
|
32
|
+
if not name.startswith("didibreak/") and ".dist-info/" not in name
|
|
33
|
+
]
|
|
34
|
+
if not any(name.endswith(".dist-info/licenses/LICENSE") for name in names):
|
|
35
|
+
problems.append("wheel has no .dist-info/licenses/LICENSE")
|
|
36
|
+
return problems
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check_sdist(sdist: Path) -> list[str]:
|
|
40
|
+
"""List problems in an sdist; empty when it is correct.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
sdist: Path to the ``.tar.gz`` file.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
One human-readable line per problem.
|
|
47
|
+
"""
|
|
48
|
+
with tarfile.open(sdist) as archive:
|
|
49
|
+
members = archive.getmembers()
|
|
50
|
+
pkg_info = next((m for m in members if m.name.endswith("/PKG-INFO")), None)
|
|
51
|
+
metadata = ""
|
|
52
|
+
if pkg_info is not None:
|
|
53
|
+
extracted = archive.extractfile(pkg_info)
|
|
54
|
+
if extracted is not None:
|
|
55
|
+
metadata = extracted.read().decode("utf-8")
|
|
56
|
+
# Member names start with the "<name>-<version>/" folder.
|
|
57
|
+
relative_names = [m.name.partition("/")[2] for m in members]
|
|
58
|
+
problems = [
|
|
59
|
+
f"unexpected file in sdist: {name}"
|
|
60
|
+
for name in relative_names
|
|
61
|
+
if name.startswith(_FORBIDDEN_SDIST_FOLDERS)
|
|
62
|
+
]
|
|
63
|
+
if "LICENSE" not in relative_names:
|
|
64
|
+
problems.append("sdist has no LICENSE")
|
|
65
|
+
if "License-File: LICENSE" not in metadata.splitlines():
|
|
66
|
+
problems.append("sdist PKG-INFO has no 'License-File: LICENSE'")
|
|
67
|
+
return problems
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
71
|
+
"""Check the single wheel and sdist in the given folder.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
argv: Arguments without the program name. Defaults to ``sys.argv[1:]``.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
0 when both archives are correct, 1 otherwise.
|
|
78
|
+
"""
|
|
79
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
80
|
+
parser.add_argument("dist_dir", type=Path, help="folder written by uv build")
|
|
81
|
+
arguments = parser.parse_args(argv)
|
|
82
|
+
|
|
83
|
+
wheels = sorted(arguments.dist_dir.glob("*.whl"))
|
|
84
|
+
sdists = sorted(arguments.dist_dir.glob("*.tar.gz"))
|
|
85
|
+
if len(wheels) != 1 or len(sdists) != 1:
|
|
86
|
+
print(
|
|
87
|
+
f"Expected one wheel and one sdist in {arguments.dist_dir}, "
|
|
88
|
+
f"found {len(wheels)} and {len(sdists)}.",
|
|
89
|
+
file=sys.stderr,
|
|
90
|
+
)
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
problems = check_wheel(wheels[0]) + check_sdist(sdists[0])
|
|
94
|
+
for problem in problems:
|
|
95
|
+
print(problem, file=sys.stderr)
|
|
96
|
+
if problems:
|
|
97
|
+
return 1
|
|
98
|
+
print(f"OK: {wheels[0].name}, {sdists[0].name}")
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
raise SystemExit(main())
|