diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 0c5637d2d..dfc435509 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ matrix.version }} - - run: git fetch origin master:origin/master --update-head-ok + - run: git fetch origin master:origin/master --update-head-ok # https://stackoverflow.com/a/76384065 - run: python -m pip install uv - run: uv pip sync --system --compile requirements.txt - run: pytest --cov-report=term-missing:skip-covered -n=auto diff --git a/pyproject.toml b/pyproject.toml index d4342dcb9..3e9554b75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dev = [ "greenlet >= 3.1.1, < 3.2", # for sqlalchemy async "hatch >= 1.14.0, < 1.15", "httpx >= 0.28.1, < 0.29", # for fastapi - "hypothesis >= 6.123.4, < 6.124", + "hypothesis >= 6.123.7, < 6.124", "ib-async-dataclass >= 1.0.3rc5, < 1.1", "img2pdf >= 0.5.1, < 0.6", "loguru >= 0.7.3, < 0.8", @@ -89,7 +89,7 @@ dev = [ ] test = [ "coverage-conditional-plugin >= 0.9.0, < 0.10", - "hypothesis >= 6.123.4, < 6.124", + "hypothesis >= 6.123.7, < 6.124", "pytest >= 8.3.4, < 8.4", "pytest-asyncio >= 0.25.1, < 0.26", "pytest-cov >= 6.0.0, < 6.1", @@ -165,7 +165,7 @@ zzz-test-hypothesis = [ "aiosqlite >= 0.20.0, < 0.21", "asyncpg >= 0.30.0, < 0.31", # for sqlalchemy async "greenlet >= 3.1.1, < 3.2", # for sqlalchemy async - "hypothesis >= 6.123.4, < 6.124", + "hypothesis >= 6.123.7, < 6.124", "numpy >= 2.0.2, < 2.1", "redis >= 5.2.1, < 5.3", "sqlalchemy >= 2.0.36, < 2.1", diff --git a/requirements.txt b/requirements.txt index c3b5c2e2b..f5dcd7ef3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -142,7 +142,7 @@ humanfriendly==10.0 # via coloredlogs hyperlink==21.0.0 # via hatch -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) ib-async-dataclass==1.0.3rc5 # via dycw-utilities (pyproject.toml) @@ -443,7 +443,7 @@ urllib3==2.3.0 # via requests userpath==1.9.2 # via hatch -uv==0.5.14 +uv==0.5.15 # via hatch uvicorn==0.34.0 # via dycw-utilities (pyproject.toml) diff --git a/requirements/altair.txt b/requirements/altair.txt index 1a68e7780..7a8f24b3d 100644 --- a/requirements/altair.txt +++ b/requirements/altair.txt @@ -19,7 +19,7 @@ deprecated==1.2.15 # via pikepdf execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) img2pdf==0.5.1 # via dycw-utilities (pyproject.toml) diff --git a/requirements/astor.txt b/requirements/astor.txt index f20f2d2cd..84103dd92 100644 --- a/requirements/astor.txt +++ b/requirements/astor.txt @@ -12,7 +12,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/asyncio.txt b/requirements/asyncio.txt index 7f4f5cf37..177ef4fea 100644 --- a/requirements/asyncio.txt +++ b/requirements/asyncio.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/atomicwrites.txt b/requirements/atomicwrites.txt index 09731b68a..218aae630 100644 --- a/requirements/atomicwrites.txt +++ b/requirements/atomicwrites.txt @@ -12,7 +12,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/atools.txt b/requirements/atools.txt index 448b0ded0..6ae2d655f 100644 --- a/requirements/atools.txt +++ b/requirements/atools.txt @@ -14,7 +14,7 @@ execnet==2.1.1 # via pytest-xdist frozendict==2.4.6 # via atools -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/beartype.txt b/requirements/beartype.txt index 785157784..9af4b3db4 100644 --- a/requirements/beartype.txt +++ b/requirements/beartype.txt @@ -12,7 +12,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/cachetools.txt b/requirements/cachetools.txt index 7f7822516..c9ac65d3f 100644 --- a/requirements/cachetools.txt +++ b/requirements/cachetools.txt @@ -12,7 +12,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/click.txt b/requirements/click.txt index e61f40241..71e10986b 100644 --- a/requirements/click.txt +++ b/requirements/click.txt @@ -12,7 +12,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/contextlib.txt b/requirements/contextlib.txt index d89838d12..a2f3b28cc 100644 --- a/requirements/contextlib.txt +++ b/requirements/contextlib.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/cryptography.txt b/requirements/cryptography.txt index 69924d007..4bcf944e1 100644 --- a/requirements/cryptography.txt +++ b/requirements/cryptography.txt @@ -14,7 +14,7 @@ cryptography==44.0.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/cvxpy.txt b/requirements/cvxpy.txt index c3b7ee3d3..beaf7ea12 100644 --- a/requirements/cvxpy.txt +++ b/requirements/cvxpy.txt @@ -14,7 +14,7 @@ cvxpy==1.6.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/dataclasses.txt b/requirements/dataclasses.txt index 663273075..e8827729e 100644 --- a/requirements/dataclasses.txt +++ b/requirements/dataclasses.txt @@ -12,7 +12,7 @@ eventkit==1.0.3 # via ib-async-dataclass execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) ib-async-dataclass==1.0.3rc5 # via dycw-utilities (pyproject.toml) diff --git a/requirements/datetime.txt b/requirements/datetime.txt index de23dd328..99dc75c57 100644 --- a/requirements/datetime.txt +++ b/requirements/datetime.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/enum.txt b/requirements/enum.txt index eac6ae84a..bed39eb61 100644 --- a/requirements/enum.txt +++ b/requirements/enum.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/errors.txt b/requirements/errors.txt index 919d439f4..cb26a0215 100644 --- a/requirements/errors.txt +++ b/requirements/errors.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/eventkit.txt b/requirements/eventkit.txt index a4b5df001..84206e518 100644 --- a/requirements/eventkit.txt +++ b/requirements/eventkit.txt @@ -12,7 +12,7 @@ eventkit==1.0.3 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/fastapi.txt b/requirements/fastapi.txt index f7d399c24..cf5f047ee 100644 --- a/requirements/fastapi.txt +++ b/requirements/fastapi.txt @@ -32,7 +32,7 @@ httpcore==1.0.7 # via httpx httpx==0.28.1 # via dycw-utilities (pyproject.toml) -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) idna==3.10 # via diff --git a/requirements/fpdf2.txt b/requirements/fpdf2.txt index fea364248..cd3aaf72f 100644 --- a/requirements/fpdf2.txt +++ b/requirements/fpdf2.txt @@ -16,7 +16,7 @@ fonttools==4.55.3 # via fpdf2 fpdf2==2.8.2 # via dycw-utilities (pyproject.toml) -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/functions.txt b/requirements/functions.txt index f034e97a6..07fc2fd92 100644 --- a/requirements/functions.txt +++ b/requirements/functions.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/functools.txt b/requirements/functools.txt index 9db74e3a4..c81b2e40a 100644 --- a/requirements/functools.txt +++ b/requirements/functools.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/getpass.txt b/requirements/getpass.txt index 5afe46374..11e068760 100644 --- a/requirements/getpass.txt +++ b/requirements/getpass.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/git.txt b/requirements/git.txt index d8e2dada0..349d98bec 100644 --- a/requirements/git.txt +++ b/requirements/git.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/hashlib.txt b/requirements/hashlib.txt index 19451c28e..0a997e5de 100644 --- a/requirements/hashlib.txt +++ b/requirements/hashlib.txt @@ -12,7 +12,7 @@ eventkit==1.0.3 # via ib-async-dataclass execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) ib-async-dataclass==1.0.3rc5 # via dycw-utilities (pyproject.toml) diff --git a/requirements/http.txt b/requirements/http.txt index 46b0189c6..f34157145 100644 --- a/requirements/http.txt +++ b/requirements/http.txt @@ -12,7 +12,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/humps.txt b/requirements/humps.txt index f02795cc7..91be10981 100644 --- a/requirements/humps.txt +++ b/requirements/humps.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/hypothesis.txt b/requirements/hypothesis.txt index 90d09873e..0103dff41 100644 --- a/requirements/hypothesis.txt +++ b/requirements/hypothesis.txt @@ -18,7 +18,7 @@ execnet==2.1.1 # via pytest-xdist greenlet==3.1.1 # via dycw-utilities (pyproject.toml) -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/ipython.txt b/requirements/ipython.txt index 04ac41129..16cbbb489 100644 --- a/requirements/ipython.txt +++ b/requirements/ipython.txt @@ -16,7 +16,7 @@ execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/iterables.txt b/requirements/iterables.txt index 8c5633576..796f6aa3f 100644 --- a/requirements/iterables.txt +++ b/requirements/iterables.txt @@ -12,7 +12,7 @@ eventkit==1.0.3 # via ib-async-dataclass execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) ib-async-dataclass==1.0.3rc5 # via dycw-utilities (pyproject.toml) diff --git a/requirements/jupyter.txt b/requirements/jupyter.txt index 64f0b0fe8..a274af297 100644 --- a/requirements/jupyter.txt +++ b/requirements/jupyter.txt @@ -64,7 +64,7 @@ httpcore==1.0.7 # via httpx httpx==0.28.1 # via jupyterlab -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) idna==3.10 # via diff --git a/requirements/logging.txt b/requirements/logging.txt index e2b479a99..cb1acc323 100644 --- a/requirements/logging.txt +++ b/requirements/logging.txt @@ -46,7 +46,7 @@ humanfriendly==10.0 # via coloredlogs hyperlink==21.0.0 # via hatch -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) idna==3.10 # via @@ -148,7 +148,7 @@ tzlocal==5.2 # via dycw-utilities (pyproject.toml) userpath==1.9.2 # via hatch -uv==0.5.14 +uv==0.5.15 # via hatch virtualenv==20.28.1 # via hatch diff --git a/requirements/loguru.txt b/requirements/loguru.txt index 467ceaa86..210e5f8e9 100644 --- a/requirements/loguru.txt +++ b/requirements/loguru.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/math.txt b/requirements/math.txt index 6b0b9fc2c..7c4107e69 100644 --- a/requirements/math.txt +++ b/requirements/math.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/memory-profiler.txt b/requirements/memory-profiler.txt index d2b79ac88..a2cc7c30a 100644 --- a/requirements/memory-profiler.txt +++ b/requirements/memory-profiler.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/modules.txt b/requirements/modules.txt index cd1706cdd..c9f2ba496 100644 --- a/requirements/modules.txt +++ b/requirements/modules.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/more-itertools.txt b/requirements/more-itertools.txt index 2affad0d2..a4f2719c7 100644 --- a/requirements/more-itertools.txt +++ b/requirements/more-itertools.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/numpy.txt b/requirements/numpy.txt index 44ac02ea2..58264865c 100644 --- a/requirements/numpy.txt +++ b/requirements/numpy.txt @@ -12,7 +12,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/optuna.txt b/requirements/optuna.txt index 2229de3ee..8efb66a11 100644 --- a/requirements/optuna.txt +++ b/requirements/optuna.txt @@ -14,7 +14,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/orjson.txt b/requirements/orjson.txt index 80bf64a8b..2e8ce81cf 100644 --- a/requirements/orjson.txt +++ b/requirements/orjson.txt @@ -12,7 +12,7 @@ eventkit==1.0.3 # via ib-async-dataclass execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) ib-async-dataclass==1.0.3rc5 # via dycw-utilities (pyproject.toml) diff --git a/requirements/os.txt b/requirements/os.txt index 4de63c634..712ef3ca2 100644 --- a/requirements/os.txt +++ b/requirements/os.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/pathlib.txt b/requirements/pathlib.txt index 7e9beb0c6..42bda7c79 100644 --- a/requirements/pathlib.txt +++ b/requirements/pathlib.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/pickle.txt b/requirements/pickle.txt index a2c08d5a7..8af94b529 100644 --- a/requirements/pickle.txt +++ b/requirements/pickle.txt @@ -12,7 +12,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/platform.txt b/requirements/platform.txt index b0c1f47cb..ca163b698 100644 --- a/requirements/platform.txt +++ b/requirements/platform.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/polars.txt b/requirements/polars.txt index e18da25f1..573f5e6e1 100644 --- a/requirements/polars.txt +++ b/requirements/polars.txt @@ -12,7 +12,7 @@ dacite==1.8.1 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/pqdm.txt b/requirements/pqdm.txt index a8c80f73d..af87d91fd 100644 --- a/requirements/pqdm.txt +++ b/requirements/pqdm.txt @@ -12,7 +12,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/pydantic.txt b/requirements/pydantic.txt index e2b419fb7..1d95de37e 100644 --- a/requirements/pydantic.txt +++ b/requirements/pydantic.txt @@ -14,7 +14,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/pyinstrument.txt b/requirements/pyinstrument.txt index 4f98f255c..e0fd4d927 100644 --- a/requirements/pyinstrument.txt +++ b/requirements/pyinstrument.txt @@ -12,7 +12,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/pyrsistent.txt b/requirements/pyrsistent.txt index 21a1ce7ef..0c67e481e 100644 --- a/requirements/pyrsistent.txt +++ b/requirements/pyrsistent.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/pytest.txt b/requirements/pytest.txt index aa2997a1f..2ddb6df41 100644 --- a/requirements/pytest.txt +++ b/requirements/pytest.txt @@ -12,7 +12,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/python-dotenv.txt b/requirements/python-dotenv.txt index 010be3c26..507e0dd54 100644 --- a/requirements/python-dotenv.txt +++ b/requirements/python-dotenv.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/random.txt b/requirements/random.txt index f4a4a064c..63321d435 100644 --- a/requirements/random.txt +++ b/requirements/random.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/re.txt b/requirements/re.txt index 6cfb37161..c15e52f08 100644 --- a/requirements/re.txt +++ b/requirements/re.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/redis.txt b/requirements/redis.txt index d3db4b787..0d9e42702 100644 --- a/requirements/redis.txt +++ b/requirements/redis.txt @@ -14,7 +14,7 @@ eventkit==1.0.3 # via ib-async-dataclass execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) ib-async-dataclass==1.0.3rc5 # via dycw-utilities (pyproject.toml) diff --git a/requirements/rich.txt b/requirements/rich.txt index adaa1b401..e04aa4834 100644 --- a/requirements/rich.txt +++ b/requirements/rich.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/scipy.txt b/requirements/scipy.txt index 7e4c148a4..3d11bfffd 100644 --- a/requirements/scipy.txt +++ b/requirements/scipy.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/sentinel.txt b/requirements/sentinel.txt index cbed841e9..ab8964514 100644 --- a/requirements/sentinel.txt +++ b/requirements/sentinel.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/slack-sdk.txt b/requirements/slack-sdk.txt index 9894379ed..97c71a1fe 100644 --- a/requirements/slack-sdk.txt +++ b/requirements/slack-sdk.txt @@ -22,7 +22,7 @@ frozenlist==1.5.0 # via # aiohttp # aiosignal -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) idna==3.10 # via yarl diff --git a/requirements/socket.txt b/requirements/socket.txt index dfb40bf17..a0d93b05d 100644 --- a/requirements/socket.txt +++ b/requirements/socket.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/sqlalchemy-polars.txt b/requirements/sqlalchemy-polars.txt index 5aa865576..1ac472e78 100644 --- a/requirements/sqlalchemy-polars.txt +++ b/requirements/sqlalchemy-polars.txt @@ -16,7 +16,7 @@ execnet==2.1.1 # via pytest-xdist greenlet==3.1.1 # via dycw-utilities (pyproject.toml) -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/sqlalchemy.txt b/requirements/sqlalchemy.txt index 72755aa43..04b4e9312 100644 --- a/requirements/sqlalchemy.txt +++ b/requirements/sqlalchemy.txt @@ -16,7 +16,7 @@ execnet==2.1.1 # via pytest-xdist greenlet==3.1.1 # via dycw-utilities (pyproject.toml) -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/streamlit.txt b/requirements/streamlit.txt index ed454e1c6..14c5af7e9 100644 --- a/requirements/streamlit.txt +++ b/requirements/streamlit.txt @@ -29,7 +29,7 @@ gitdb==4.0.12 # via gitpython gitpython==3.1.44 # via streamlit -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) idna==3.10 # via requests diff --git a/requirements/sys.txt b/requirements/sys.txt index 65ad7ca21..0aa6ad0f7 100644 --- a/requirements/sys.txt +++ b/requirements/sys.txt @@ -40,7 +40,7 @@ httpx==0.28.1 # via hatch hyperlink==21.0.0 # via hatch -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) idna==3.10 # via @@ -140,7 +140,7 @@ tzlocal==5.2 # via dycw-utilities (pyproject.toml) userpath==1.9.2 # via hatch -uv==0.5.14 +uv==0.5.15 # via hatch virtualenv==20.28.1 # via hatch diff --git a/requirements/tempfile.txt b/requirements/tempfile.txt index b2d55c6e9..cbd71697c 100644 --- a/requirements/tempfile.txt +++ b/requirements/tempfile.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/tenacity.txt b/requirements/tenacity.txt index 414a23bc9..33cede225 100644 --- a/requirements/tenacity.txt +++ b/requirements/tenacity.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/text.txt b/requirements/text.txt index 7d6c454ec..59402aff3 100644 --- a/requirements/text.txt +++ b/requirements/text.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/threading.txt b/requirements/threading.txt index 465f10b5a..25e7e578a 100644 --- a/requirements/threading.txt +++ b/requirements/threading.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/timer.txt b/requirements/timer.txt index 5b97b3070..518e32e98 100644 --- a/requirements/timer.txt +++ b/requirements/timer.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/traceback.txt b/requirements/traceback.txt index 108cb1d58..895cf21af 100644 --- a/requirements/traceback.txt +++ b/requirements/traceback.txt @@ -40,7 +40,7 @@ httpx==0.28.1 # via hatch hyperlink==21.0.0 # via hatch -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) idna==3.10 # via @@ -140,7 +140,7 @@ tzlocal==5.2 # via dycw-utilities (pyproject.toml) userpath==1.9.2 # via hatch -uv==0.5.14 +uv==0.5.15 # via hatch virtualenv==20.28.1 # via hatch diff --git a/requirements/treelib.txt b/requirements/treelib.txt index 957abd133..a4cf1299b 100644 --- a/requirements/treelib.txt +++ b/requirements/treelib.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/types.txt b/requirements/types.txt index 22c4f2a82..01954440b 100644 --- a/requirements/types.txt +++ b/requirements/types.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/typing.txt b/requirements/typing.txt index 712202356..b4d3ae71a 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -14,7 +14,7 @@ eventkit==1.0.3 # via ib-async-dataclass execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) ib-async-dataclass==1.0.3rc5 # via dycw-utilities (pyproject.toml) diff --git a/requirements/uuid.txt b/requirements/uuid.txt index c5627454a..a52c5057f 100644 --- a/requirements/uuid.txt +++ b/requirements/uuid.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/version.txt b/requirements/version.txt index 6de884a3d..e7965a26a 100644 --- a/requirements/version.txt +++ b/requirements/version.txt @@ -38,7 +38,7 @@ httpx==0.28.1 # via hatch hyperlink==21.0.0 # via hatch -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) idna==3.10 # via @@ -134,7 +134,7 @@ typing-extensions==4.12.2 # anyio userpath==1.9.2 # via hatch -uv==0.5.14 +uv==0.5.15 # via hatch virtualenv==20.28.1 # via hatch diff --git a/requirements/warnings.txt b/requirements/warnings.txt index 92cf3e246..d0fcd344c 100644 --- a/requirements/warnings.txt +++ b/requirements/warnings.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/whenever.txt b/requirements/whenever.txt index 06119bd58..c5236da21 100644 --- a/requirements/whenever.txt +++ b/requirements/whenever.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/zipfile.txt b/requirements/zipfile.txt index 1e8d4563e..d2c3ee5e5 100644 --- a/requirements/zipfile.txt +++ b/requirements/zipfile.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/requirements/zoneinfo.txt b/requirements/zoneinfo.txt index 4724aba24..6a05a9ddc 100644 --- a/requirements/zoneinfo.txt +++ b/requirements/zoneinfo.txt @@ -10,7 +10,7 @@ coverage-conditional-plugin==0.9.0 # via dycw-utilities (pyproject.toml) execnet==2.1.1 # via pytest-xdist -hypothesis==6.123.4 +hypothesis==6.123.7 # via dycw-utilities (pyproject.toml) iniconfig==2.0.0 # via pytest diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 58b96da88..df2c2e47f 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -33,6 +33,11 @@ # fixtures +@fixture +def git_ref() -> str: + return "origin/master" if IS_CI else "master" + + @fixture def traceback_func_chain() -> Pattern[str]: return re.compile( diff --git a/src/tests/test_hypothesis.py b/src/tests/test_hypothesis.py index 96e57135b..7b63da9ed 100644 --- a/src/tests/test_hypothesis.py +++ b/src/tests/test_hypothesis.py @@ -41,7 +41,12 @@ is_local_datetime, is_zoned_datetime, ) -from utilities.git import _GIT_REMOTE_GET_URL_ORIGIN, _GIT_REV_PARSE_ABBREV_REV_HEAD +from utilities.git import ( + _GIT_REMOTE_GET_URL_ORIGIN, + _GIT_REV_PARSE_ABBREV_REV_HEAD, + _GIT_TAG_POINTS_AT, + MASTER, +) from utilities.hypothesis import ( _SQLALCHEMY_ENGINE_DIALECTS, _ZONED_DATETIMES_LEFT_MOST, @@ -80,6 +85,7 @@ text_digits, text_printable, timedeltas_2w, + triples, uint32s, uint64s, versions, @@ -426,12 +432,22 @@ def test_finite_and_integral( class TestGitRepos: @given(data=data()) - @settings_with_reduced_examples(suppress_health_check={HealthCheck.filter_too_much}) + @settings_with_reduced_examples() def test_main(self, *, data: DataObject) -> None: branch = data.draw(text_ascii(min_size=1) | none()) remote = data.draw(text_ascii(min_size=1) | none()) - root = data.draw(git_repos(branch=branch, remote=remote)) - assert set(root.iterdir()) == {Path(root, ".git")} + git_version = data.draw(versions() | none()) + hatch_version = data.draw(versions() | none()) + root = data.draw( + git_repos( + branch=branch, + remote=remote, + git_version=git_version, + hatch_version=hatch_version, + ) + ) + files = set(root.iterdir()) + assert Path(root, ".git") in files if branch is not None: output = check_output( _GIT_REV_PARSE_ABBREV_REV_HEAD, stderr=PIPE, cwd=root, text=True @@ -442,6 +458,30 @@ def test_main(self, *, data: DataObject) -> None: _GIT_REMOTE_GET_URL_ORIGIN, stderr=PIPE, cwd=root, text=True ) assert output.strip("\n") == remote + if git_version is not None: + output = check_output( + [*_GIT_TAG_POINTS_AT, MASTER], stderr=PIPE, cwd=root, text=True + ) + assert output.strip("\n") == str(git_version) + if hatch_version is not None: + assert { + Path(root, "LICENSE.txt"), + Path(root, "README.md"), + Path(root, "pyproject.toml"), + Path(root, "src"), + Path(root, "tests"), + }.issubset(files) + output = check_output( + ["hatch", "version"], stderr=PIPE, cwd=root, text=True + ) + assert output.strip("\n") == str(hatch_version) + + @given(data=data()) + @settings(max_examples=1) + def test_hatch_version_001(self, *, data: DataObject) -> None: + root = data.draw(git_repos(hatch_version=Version(0, 0, 1))) + output = check_output(["hatch", "version"], stderr=PIPE, cwd=root, text=True) + assert output.strip("\n") == "0.0.1" class TestHashables: @@ -875,6 +915,20 @@ def test_main( assert min_value <= timedelta <= max_value +class TestTriples: + @given(data=data(), unique=booleans(), sorted_=booleans()) + def test_main(self, *, data: DataObject, unique: bool, sorted_: bool) -> None: + result = data.draw(triples(integers(), unique=unique, sorted=sorted_)) + assert isinstance(result, tuple) + assert len(result) == 3 + first, second, third = result + if unique: + assert first != second + assert second != third + if sorted_: + assert first <= second <= third + + class TestUInt32s: @given(data=data(), min_value=uint32s(), max_value=uint32s()) def test_main(self, *, data: DataObject, min_value: int, max_value: int) -> None: @@ -892,10 +946,14 @@ def test_main(self, *, data: DataObject, min_value: int, max_value: int) -> None class TestVersions: - @given(data=data()) - def test_main(self, *, data: DataObject) -> None: - version = data.draw(versions()) + @given(data=data(), suffix=booleans()) + def test_main(self, *, data: DataObject, suffix: bool) -> None: + version = data.draw(versions(suffix=suffix)) assert isinstance(version, Version) + if suffix: + assert version.suffix is not None + else: + assert version.suffix is None @SKIPIF_CI_AND_NOT_LINUX diff --git a/src/tests/test_logging.py b/src/tests/test_logging.py index 1874dd16c..495ce08ad 100644 --- a/src/tests/test_logging.py +++ b/src/tests/test_logging.py @@ -106,10 +106,10 @@ def test_main(self) -> None: class TestSetupLogging: @skipif_windows def test_decorated( - self, *, tmp_path: Path, traceback_func_one: Pattern[str] + self, *, tmp_path: Path, git_ref: str, traceback_func_one: Pattern[str] ) -> None: name = str(tmp_path) - setup_logging(logger=name, files_dir=tmp_path) + setup_logging(logger=name, git_ref=git_ref, files_dir=tmp_path) logger = getLogger(name) assert len(logger.handlers) == 7 self.assert_files(tmp_path, "init") @@ -121,10 +121,10 @@ def test_decorated( @skipif_windows def test_undecorated( - self, *, tmp_path: Path, traceback_func_untraced: Pattern[str] + self, *, tmp_path: Path, git_ref: str, traceback_func_untraced: Pattern[str] ) -> None: name = str(tmp_path) - setup_logging(logger=name, files_dir=tmp_path) + setup_logging(logger=name, git_ref=git_ref, files_dir=tmp_path) logger = getLogger(name) assert len(logger.handlers) == 7 self.assert_files(tmp_path, "init") @@ -136,10 +136,10 @@ def test_undecorated( @skipif_windows def test_regular_percent_formatting( - self, *, caplog: LogCaptureFixture, tmp_path: Path + self, *, tmp_path: Path, git_ref: str, caplog: LogCaptureFixture ) -> None: name = str(tmp_path) - setup_logging(logger=name, files_dir=tmp_path) + setup_logging(logger=name, git_ref=git_ref, files_dir=tmp_path) logger = getLogger(name) logger.info("int: %d, float: %.2f", 1, 12.3456) record = one(caplog.records) @@ -149,10 +149,10 @@ def test_regular_percent_formatting( @skipif_windows def test_new_brace_formatting( - self, *, caplog: LogCaptureFixture, tmp_path: Path + self, *, tmp_path: Path, git_ref: str, caplog: LogCaptureFixture ) -> None: name = str(tmp_path) - setup_logging(logger=name, files_dir=tmp_path) + setup_logging(logger=name, git_ref=git_ref, files_dir=tmp_path) logger = getLogger(name) logger.info("int: {:d}, float: {:.2f}, percent: {:.2%}", 1, 12.3456, 0.123456) record = one(caplog.records) @@ -161,16 +161,20 @@ def test_new_brace_formatting( assert record.message == expected @skipif_windows - def test_no_console(self, *, tmp_path: Path) -> None: + def test_no_console(self, *, tmp_path: Path, git_ref: str) -> None: name = str(tmp_path) - setup_logging(logger=name, console_level=None, files_dir=tmp_path) + setup_logging( + logger=name, console_level=None, git_ref=git_ref, files_dir=tmp_path + ) logger = getLogger(name) assert len(logger.handlers) == 5 @skipif_windows - def test_zoned_datetime(self, *, caplog: LogCaptureFixture, tmp_path: Path) -> None: + def test_zoned_datetime( + self, *, tmp_path: Path, git_ref: str, caplog: LogCaptureFixture + ) -> None: name = str(tmp_path) - setup_logging(logger=name, files_dir=tmp_path) + setup_logging(logger=name, git_ref=git_ref, files_dir=tmp_path) logger = getLogger(name) logger.info("") record = one(caplog.records) @@ -179,7 +183,7 @@ def test_zoned_datetime(self, *, caplog: LogCaptureFixture, tmp_path: Path) -> N assert isinstance(record._zoned_datetime_str, str) @skipif_windows - def test_extra(self, *, tmp_path: Path) -> None: + def test_extra(self, *, tmp_path: Path, git_ref: str) -> None: name = str(tmp_path) def extra(logger: LoggerOrName | None, /) -> None: @@ -187,7 +191,7 @@ def extra(logger: LoggerOrName | None, /) -> None: handler.setLevel(DEBUG) get_logger(logger=logger).addHandler(handler) - setup_logging(logger=name, files_dir=tmp_path, extra=extra) + setup_logging(logger=name, git_ref=git_ref, files_dir=tmp_path, extra=extra) logger = getLogger(name) logger.info("") files = list(tmp_path.iterdir()) diff --git a/src/tests/test_sys.py b/src/tests/test_sys.py index f676a9dfd..9c4832f0c 100644 --- a/src/tests/test_sys.py +++ b/src/tests/test_sys.py @@ -38,11 +38,12 @@ def test_with_setup_logging_decorated( self, *, tmp_path: Path, + git_ref: str, caplog: LogCaptureFixture, traceback_func_one: Pattern[str], ) -> None: name = str(tmp_path) - setup_logging(logger=name, files_dir=tmp_path) + setup_logging(logger=name, git_ref=git_ref, files_dir=tmp_path) hook = make_except_hook(logger=name) self._assert_files_and_caplog(tmp_path, caplog, "init") try: @@ -56,11 +57,12 @@ def test_with_setup_logging_undecorated( self, *, tmp_path: Path, + git_ref: str, caplog: LogCaptureFixture, traceback_func_untraced: Pattern[str], ) -> None: name = str(tmp_path) - setup_logging(logger=name, files_dir=tmp_path) + setup_logging(logger=name, git_ref=git_ref, files_dir=tmp_path) hook = make_except_hook(logger=name) self._assert_files_and_caplog(tmp_path, caplog, "init") try: diff --git a/src/tests/test_traceback.py b/src/tests/test_traceback.py index 3543dc1e2..c56c19594 100644 --- a/src/tests/test_traceback.py +++ b/src/tests/test_traceback.py @@ -96,10 +96,10 @@ def test_summary(self) -> None: class TestGetRichTraceback: - def test_func_one(self, *, traceback_func_one: Pattern[str]) -> None: + def test_func_one(self, *, git_ref: str, traceback_func_one: Pattern[str]) -> None: with raises(AssertionError) as exc_info: _ = func_one(1, 2, 3, 4, c=5, d=6, e=7) - exc_tb = get_rich_traceback(exc_info.value) + exc_tb = get_rich_traceback(exc_info.value, git_ref=git_ref) assert isinstance(exc_tb, ExcTB) assert len(exc_tb) == 1 frame = one(exc_tb) @@ -119,10 +119,10 @@ def test_func_one(self, *, traceback_func_one: Pattern[str]) -> None: assert traceback_func_one.search(repr(exc_tb)) - def test_func_two(self, *, traceback_func_two: Pattern[str]) -> None: + def test_func_two(self, *, git_ref: str, traceback_func_two: Pattern[str]) -> None: with raises(AssertionError) as exc_info: _ = func_two_first(1, 2, 3, 4, c=5, d=6, e=7) - exc_tb = get_rich_traceback(exc_info.value) + exc_tb = get_rich_traceback(exc_info.value, git_ref=git_ref) assert isinstance(exc_tb, ExcTB) assert len(exc_tb) == 2 frame1, frame2 = exc_tb @@ -151,10 +151,10 @@ def test_func_two(self, *, traceback_func_two: Pattern[str]) -> None: assert traceback_func_two.search(repr(exc_tb)) - def test_func_beartype(self) -> None: + def test_func_beartype(self, *, git_ref: str) -> None: with raises(AssertionError) as exc_info: _ = func_beartype(1, 2, 3, 4, c=5, d=6, e=7) - exc_tb = get_rich_traceback(exc_info.value) + exc_tb = get_rich_traceback(exc_info.value, git_ref=git_ref) assert isinstance(exc_tb, ExcTB) assert len(exc_tb) == 1 frame = one(exc_tb) @@ -172,10 +172,10 @@ def test_func_beartype(self) -> None: assert frame.locals["kwargs"] == {"d": 12, "e": 14} assert isinstance(exc_tb.error, AssertionError) - def test_func_beartype_error(self) -> None: + def test_func_beartype_error(self, *, git_ref: str) -> None: with raises(BeartypeCallHintReturnViolation) as exc_info: _ = func_beartype_error_first(1, 2, 3, 4, c=5, d=6, e=7) - exc_tb = get_rich_traceback(exc_info.value) + exc_tb = get_rich_traceback(exc_info.value, git_ref=git_ref) assert isinstance(exc_tb, ExcTB) assert len(exc_tb) == 2 frame1, frame2 = exc_tb @@ -200,10 +200,12 @@ def test_func_beartype_error(self) -> None: assert frame2.locals["kwargs"] == {"c": 10, "d": 12, "e": 14} assert isinstance(exc_tb.error, BeartypeCallHintReturnViolation) - def test_func_chain(self, *, traceback_func_chain: Pattern[str]) -> None: + def test_func_chain( + self, *, git_ref: str, traceback_func_chain: Pattern[str] + ) -> None: with raises(ValueError, match=".*") as exc_info: _ = func_chain_first(1, 2, 3, 4, c=5, d=6, e=7) - exc_chain = get_rich_traceback(exc_info.value) + exc_chain = get_rich_traceback(exc_info.value, git_ref=git_ref) assert isinstance(exc_chain, ExcChainTB) assert len(exc_chain) == 2 exc_tb1, exc_tb2 = exc_chain @@ -236,25 +238,25 @@ def test_func_chain(self, *, traceback_func_chain: Pattern[str]) -> None: assert traceback_func_chain.search(repr(exc_chain)) - def test_func_decorated_sync(self) -> None: + def test_func_decorated_sync(self, *, git_ref: str) -> None: with raises(AssertionError) as exc_info: _ = func_decorated_sync_first(1, 2, 3, 4, c=5, d=6, e=7) - exc_path = get_rich_traceback(exc_info.value) + exc_path = get_rich_traceback(exc_info.value, git_ref=git_ref) assert isinstance(exc_path, ExcTB) self._assert_decorated(exc_path, "sync") assert len(exc_path) == 5 - async def test_func_decorated_async(self) -> None: + async def test_func_decorated_async(self, *, git_ref: str) -> None: with raises(AssertionError) as exc_info: _ = await func_decorated_async_first(1, 2, 3, 4, c=5, d=6, e=7) - error = get_rich_traceback(exc_info.value) + error = get_rich_traceback(exc_info.value, git_ref=git_ref) assert isinstance(error, ExcTB) self._assert_decorated(error, "async") - def test_func_recursive(self) -> None: + def test_func_recursive(self, *, git_ref: str) -> None: with raises(AssertionError) as exc_info: _ = func_recursive(1, 2, 3, 4, c=5, d=6, e=7) - exc_path = get_rich_traceback(exc_info.value) + exc_path = get_rich_traceback(exc_info.value, git_ref=git_ref) assert isinstance(exc_path, ExcTB) assert len(exc_path) == 2 frame1, frame2 = exc_path @@ -281,48 +283,48 @@ def test_func_recursive(self) -> None: assert frame2.locals["kwargs"] == {"d": 24, "e": 28} assert isinstance(exc_path.error, AssertionError) - def test_func_runtime_sync(self) -> None: + def test_func_runtime_sync(self, *, git_ref: str) -> None: with raises(AssertionError) as exc_info1: _ = func_runtime_sync(1, 2, 3, 4, c=5, d=6, e=7) - exc_path1 = get_rich_traceback(exc_info1.value) + exc_path1 = get_rich_traceback(exc_info1.value, git_ref=git_ref) assert isinstance(exc_path1, ExcTB) with disable_trace_for_func_runtime_sync(): with raises(AssertionError) as exc_info2: _ = func_runtime_sync(1, 2, 3, 4, c=5, d=6, e=7) - exc_path2 = get_rich_traceback(exc_info2.value) + exc_path2 = get_rich_traceback(exc_info2.value, git_ref=git_ref) assert isinstance(exc_path2, AssertionError) with raises(AssertionError) as exc_info3: _ = func_runtime_sync(1, 2, 3, 4, c=5, d=6, e=7) - exc_path3 = get_rich_traceback(exc_info3.value) + exc_path3 = get_rich_traceback(exc_info3.value, git_ref=git_ref) assert isinstance(exc_path3, ExcTB) - async def test_func_runtime_async(self) -> None: + async def test_func_runtime_async(self, *, git_ref: str) -> None: with raises(AssertionError) as exc_info1: _ = await func_runtime_async(1, 2, 3, 4, c=5, d=6, e=7) - exc_path1 = get_rich_traceback(exc_info1.value) + exc_path1 = get_rich_traceback(exc_info1.value, git_ref=git_ref) assert isinstance(exc_path1, ExcTB) with disable_trace_for_func_runtime_async(): with raises(AssertionError) as exc_info2: _ = await func_runtime_async(1, 2, 3, 4, c=5, d=6, e=7) - exc_path2 = get_rich_traceback(exc_info2.value) + exc_path2 = get_rich_traceback(exc_info2.value, git_ref=git_ref) assert isinstance(exc_path2, AssertionError) with raises(AssertionError) as exc_info3: _ = await func_runtime_async(1, 2, 3, 4, c=5, d=6, e=7) - exc_path3 = get_rich_traceback(exc_info3.value) + exc_path3 = get_rich_traceback(exc_info3.value, git_ref=git_ref) assert isinstance(exc_path3, ExcTB) - def test_func_setup(self) -> None: + def test_func_setup(self, *, git_ref: str) -> None: with raises(AssertionError) as exc_info1: _ = func_setup(1, 2, 3, 4, c=5, d=6, e=7) - exc_path1 = get_rich_traceback(exc_info1.value) + exc_path1 = get_rich_traceback(exc_info1.value, git_ref=git_ref) assert isinstance(exc_path1, AssertionError) async def test_func_task_group_one( - self, *, traceback_func_task_group_one: Pattern[str] + self, *, git_ref: str, traceback_func_task_group_one: Pattern[str] ) -> None: with raises(ExceptionGroup) as exc_info: await func_task_group_one_first(1, 2, 3, 4, c=5, d=6, e=7) - exc_group = get_rich_traceback(exc_info.value) + exc_group = get_rich_traceback(exc_info.value, git_ref=git_ref) assert isinstance(exc_group, ExcGroupTB) assert exc_group.path is not None assert len(exc_group.path) == 1 @@ -361,10 +363,10 @@ async def test_func_task_group_one( @FLAKY @SKIPIF_CI - async def test_func_task_group_two(self) -> None: + async def test_func_task_group_two(self, *, git_ref: str) -> None: with raises(ExceptionGroup) as exc_info: await func_task_group_two_first(1, 2, 3, 4, c=5, d=6, e=7) - exc_group = get_rich_traceback(exc_info.value) + exc_group = get_rich_traceback(exc_info.value, git_ref=git_ref) assert isinstance(exc_group, ExcGroupTB) assert exc_group.path is not None assert len(exc_group.path) == 1 @@ -414,20 +416,20 @@ async def test_func_task_group_two(self) -> None: assert frame2.locals["kwargs"] == {"d": 26, "e": 30} assert isinstance(exc_path2.error, AssertionError) - def test_func_untraced(self) -> None: + def test_func_untraced(self, *, git_ref: str) -> None: with raises(AssertionError) as exc_info: _ = func_untraced(1, 2, 3, 4, c=5, d=6, e=7) - error = get_rich_traceback(exc_info.value) + error = get_rich_traceback(exc_info.value, git_ref=git_ref) assert isinstance(error, AssertionError) - def test_custom_error(self) -> None: + def test_custom_error(self, *, git_ref: str) -> None: @trace def raises_custom_error() -> bool: return one([True, False]) with raises(OneNonUniqueError) as exc_info: _ = raises_custom_error() - exc_path = get_rich_traceback(exc_info.value) + exc_path = get_rich_traceback(exc_info.value, git_ref=git_ref) assert isinstance(exc_path, ExcTB) assert exc_path.error.first is True assert exc_path.error.second is False @@ -521,12 +523,12 @@ def _assert_decorated( class TestRichTracebackFormatter: def test_decorated( - self, *, tmp_path: Path, traceback_func_one: Pattern[str] + self, *, tmp_path: Path, git_ref: str, traceback_func_one: Pattern[str] ) -> None: logger = getLogger(str(tmp_path)) logger.setLevel(DEBUG) handler = StreamHandler(buffer := StringIO()) - handler.setFormatter(RichTracebackFormatter(detail=True)) + handler.setFormatter(RichTracebackFormatter(git_ref=git_ref, detail=True)) handler.setLevel(DEBUG) logger.addHandler(handler) try: @@ -537,12 +539,12 @@ def test_decorated( assert traceback_func_one.search(result) def test_undecorated( - self, *, tmp_path: Path, traceback_func_untraced: Pattern[str] + self, *, tmp_path: Path, git_ref: str, traceback_func_untraced: Pattern[str] ) -> None: logger = getLogger(str(tmp_path)) logger.setLevel(DEBUG) handler = StreamHandler(buffer := StringIO()) - handler.setFormatter(RichTracebackFormatter(detail=True)) + handler.setFormatter(RichTracebackFormatter(git_ref=git_ref, detail=True)) handler.setLevel(DEBUG) logger.addHandler(handler) try: @@ -552,17 +554,17 @@ def test_undecorated( result = buffer.getvalue() assert traceback_func_untraced.search(result) - def test_create_and_set(self) -> None: + def test_create_and_set(self, *, git_ref: str) -> None: handler = StreamHandler() assert len(handler.filters) == 0 - _ = RichTracebackFormatter.create_and_set(handler) + _ = RichTracebackFormatter.create_and_set(handler, git_ref=git_ref) assert len(handler.filters) == 1 - def test_no_logging(self, *, tmp_path: Path) -> None: + def test_no_logging(self, *, tmp_path: Path, git_ref: str) -> None: logger = getLogger(str(tmp_path)) logger.setLevel(ERROR) handler = StreamHandler(buffer := StringIO()) - handler.setFormatter(RichTracebackFormatter(detail=True)) + handler.setFormatter(RichTracebackFormatter(git_ref=git_ref, detail=True)) handler.setLevel(DEBUG) logger.addHandler(handler) logger.error("message") @@ -570,12 +572,14 @@ def test_no_logging(self, *, tmp_path: Path) -> None: expected = "ERROR: record.exc_info=None\n" assert result == expected - def test_post(self, *, tmp_path: Path) -> None: + def test_post(self, *, tmp_path: Path, git_ref: str) -> None: logger = getLogger(str(tmp_path)) logger.setLevel(DEBUG) handler = StreamHandler(buffer := StringIO()) handler.setFormatter( - RichTracebackFormatter(detail=True, post=lambda x: f"> {x}") + RichTracebackFormatter( + git_ref=git_ref, detail=True, post=lambda x: f"> {x}" + ) ) handler.setLevel(DEBUG) logger.addHandler(handler) diff --git a/src/tests/test_version.py b/src/tests/test_version.py index df20125f7..a28f5cd1a 100644 --- a/src/tests/test_version.py +++ b/src/tests/test_version.py @@ -1,17 +1,29 @@ from __future__ import annotations -from hypothesis import given -from hypothesis.strategies import integers, none +from re import search + +from hypothesis import assume, given, settings +from hypothesis.strategies import ( + DataObject, + booleans, + data, + integers, + none, + sampled_from, +) from pytest import raises -from utilities.hypothesis import text_ascii, versions +from utilities.git import MASTER +from utilities.hypothesis import git_repos, pairs, text_ascii, versions from utilities.version import ( + GetVersionError, ParseVersionError, Version, _VersionEmptySuffixError, _VersionNegativeMajorVersionError, _VersionNegativeMinorVersionError, _VersionNegativePatchVersionError, + _VersionZeroError, get_git_version, get_hatch_version, get_version, @@ -20,21 +32,66 @@ class TestGetGitVersion: - def test_main(self) -> None: - version = get_git_version() - assert isinstance(version, Version) + @given(data=data(), version=versions()) + @settings(max_examples=1) + def test_main(self, *, data: DataObject, version: Version) -> None: + repo = data.draw(git_repos(git_version=version)) + result = get_git_version(cwd=repo, ref=MASTER) + assert result == version class TestGetHatchVersion: - def test_main(self) -> None: - version = get_hatch_version() - assert isinstance(version, Version) + @given(data=data(), version=versions()) + @settings(max_examples=1) + def test_main(self, *, data: DataObject, version: Version) -> None: + repo = data.draw(git_repos(hatch_version=version)) + result = get_hatch_version(cwd=repo) + assert result == version class TestGetVersion: - def test_main(self) -> None: - version = get_version() - assert isinstance(version, Version) + @given(data=data(), version=versions()) + @settings(max_examples=1) + def test_equal(self, *, data: DataObject, version: Version) -> None: + repo = data.draw(git_repos(git_version=version, hatch_version=version)) + result = get_version(cwd=repo, ref=MASTER) + assert result == version + + @given(data=data(), versions=pairs(versions(), unique=True, sorted=True)) + @settings(max_examples=1) + def test_behind( + self, *, data: DataObject, versions: tuple[Version, Version] + ) -> None: + hatch, git = versions + repo = data.draw(git_repos(git_version=git, hatch_version=hatch)) + result = get_version(cwd=repo, ref=MASTER) + expected = hatch.with_suffix(suffix="behind") + assert result == expected + + @given(data=data(), git=versions()) + @settings(max_examples=1) + def test_dirty(self, *, data: DataObject, git: Version) -> None: + hatch = data.draw( + sampled_from([git.bump_major(), git.bump_minor(), git.bump_patch()]) + ) + repo = data.draw(git_repos(git_version=git, hatch_version=hatch)) + result = get_version(cwd=repo, ref=MASTER) + expected = hatch.with_suffix(suffix="dirty") + assert result == expected + + @given(data=data(), versions=pairs(versions(), unique=True, sorted=True)) + @settings(max_examples=1) + def test_error( + self, *, data: DataObject, versions: tuple[Version, Version] + ) -> None: + git, hatch = versions + _ = assume(hatch not in [git.bump_major(), git.bump_minor(), git.bump_patch()]) + repo = data.draw(git_repos(git_version=git, hatch_version=hatch)) + with raises( + GetVersionError, + match="`hatch` version is ahead of `git` version in an incompatible way; got .* and .*", + ): + _ = get_version(cwd=repo, ref=MASTER) class TestParseVersion: @@ -57,6 +114,11 @@ def test_hashable(self, *, version: Version) -> None: def test_orderable(self, *, version1: Version, version2: Version) -> None: assert (version1 <= version2) or (version1 >= version2) + @given(version=versions(suffix=booleans())) + def test_repr(self, *, version: Version) -> None: + result = repr(version) + assert search(r"^\d+\.\d+\.\d+", result) + @given(version=versions()) def test_bump_major(self, *, version: Version) -> None: bumped = version.bump_major() @@ -92,6 +154,17 @@ def test_with_suffix(self, *, version: Version, suffix: str | None) -> None: assert new.patch == version.patch assert new.suffix == suffix + @given(version=versions()) + def test_error_order(self, *, version: Version) -> None: + with raises(TypeError): + _ = version <= None + + def test_error_zero(self) -> None: + with raises( + _VersionZeroError, match="Version must be greater than zero; got 0.0.0" + ): + _ = Version(0, 0, 0) + @given(major=integers(max_value=-1)) def test_error_negative_major_version(self, *, major: int) -> None: with raises( diff --git a/src/utilities/__init__.py b/src/utilities/__init__.py index 7d96457f8..a9f825860 100644 --- a/src/utilities/__init__.py +++ b/src/utilities/__init__.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "0.90.6" +__version__ = "0.90.7" diff --git a/src/utilities/git.py b/src/utilities/git.py index 32d050406..31ac1e6d2 100644 --- a/src/utilities/git.py +++ b/src/utilities/git.py @@ -13,8 +13,22 @@ if TYPE_CHECKING: from utilities.types import PathLike -_GIT_REV_PARSE_ABBREV_REV_HEAD = ["git", "rev-parse", "--abbrev-ref", "HEAD"] +MASTER = "master" +ORIGIN_MASTER = "origin/master" _GIT_REMOTE_GET_URL_ORIGIN = ["git", "remote", "get-url", "origin"] +_GIT_REV_PARSE_ABBREV_REV_HEAD = ["git", "rev-parse", "--abbrev-ref", "HEAD"] +_GIT_TAG_POINTS_AT = ["git", "tag", "--points-at"] + + +## + + +def fetch_all_tags(*, cwd: PathLike = PWD) -> None: + """Fetch the tags.""" + _ = check_call(["git", "fetch", "--all", "--tags"], cwd=cwd) + + +## def get_branch_name(*, cwd: PathLike = PWD) -> str: @@ -25,20 +39,27 @@ def get_branch_name(*, cwd: PathLike = PWD) -> str: return output.strip("\n") +## + + def get_ref_tags(ref: str, /, *, cwd: PathLike = PWD) -> list[str]: """Get the tags of a reference.""" - output = check_output( - ["git", "tag", "--points-at", ref], stderr=PIPE, cwd=cwd, text=True - ) + output = check_output([*_GIT_TAG_POINTS_AT, ref], stderr=PIPE, cwd=cwd, text=True) return output.strip("\n").splitlines() +## + + def get_repo_name(*, cwd: PathLike = PWD) -> str: """Get the repo name.""" output = check_output(_GIT_REMOTE_GET_URL_ORIGIN, stderr=PIPE, cwd=cwd, text=True) return Path(output.strip("\n")).stem # not valid_path +## + + def get_repo_root(*, cwd: PathLike = PWD) -> Path: """Get the repo root.""" try: @@ -55,11 +76,6 @@ def get_repo_root(*, cwd: PathLike = PWD) -> Path: return Path(output.strip("\n")) -def fetch_all_tags(*, cwd: PathLike = PWD) -> None: - """Fetch the tags.""" - _ = check_call(["git", "fetch", "--all", "--tags"], cwd=cwd) - - @dataclass(kw_only=True, slots=True) class GetRepoRootError(Exception): cwd: PathLike @@ -70,6 +86,8 @@ def __str__(self) -> str: __all__ = [ + "MASTER", + "ORIGIN_MASTER", "GetRepoRootError", "fetch_all_tags", "get_branch_name", diff --git a/src/utilities/hypothesis.py b/src/utilities/hypothesis.py index 5f8274ffb..eb96d53c6 100644 --- a/src/utilities/hypothesis.py +++ b/src/utilities/hypothesis.py @@ -19,10 +19,11 @@ from datetime import timezone from enum import Enum, auto from functools import partial -from math import ceil, floor, inf, isfinite, nan +from math import ceil, floor, inf, isclose, isfinite, nan from os import environ from pathlib import Path from re import search +from shutil import move, rmtree from string import ascii_letters, ascii_lowercase, ascii_uppercase, digits, printable from subprocess import check_call from typing import TYPE_CHECKING, Any, Protocol, TypeVar, assert_never, cast, overload @@ -35,7 +36,6 @@ DrawFn, SearchStrategy, booleans, - builds, characters, composite, dates, @@ -358,11 +358,13 @@ def git_repos( *, branch: MaybeSearchStrategy[str | None] = None, remote: MaybeSearchStrategy[str | None] = None, + git_version: MaybeSearchStrategy[Version | None] = None, + hatch_version: MaybeSearchStrategy[Version | None] = None, ) -> Path: draw = lift_draw(_draw) path = draw(temp_paths()) with temp_cwd(path): - _ = check_call(["git", "init"]) + _ = check_call(["git", "init", "-b", "master"]) _ = check_call(["git", "config", "user.name", "User"]) _ = check_call(["git", "config", "user.email", "a@z.com"]) file = Path(path, "file") @@ -372,10 +374,20 @@ def git_repos( _ = check_call(["git", "commit", "-m", "add"]) _ = check_call(["git", "rm", file_str]) _ = check_call(["git", "commit", "-m", "rm"]) - if (branch := draw(branch)) is not None: - _ = check_call(["git", "checkout", "-b", branch]) - if (remote := draw(remote)) is not None: - _ = check_call(["git", "remote", "add", "origin", remote]) + if (branch_ := draw(branch)) is not None: + _ = check_call(["git", "checkout", "-b", branch_]) + if (remote_ := draw(remote)) is not None: + _ = check_call(["git", "remote", "add", "origin", remote_]) + if (git_version_ := draw(git_version)) is not None: + _ = check_call(["git", "tag", str(git_version_), "master"]) + if (hatch_version_ := draw(hatch_version)) is not None: + _ = check_call(["hatch", "new", "package"]) + package = path.joinpath("package") + for p in package.iterdir(): + move(p, p.parent.with_name(p.name)) + rmtree(package) + if (hatch_version_ > Version(0, 0, 1)) and (hatch_version_.suffix is None): + _ = check_call(["hatch", "version", str(hatch_version_)]) return path @@ -550,6 +562,13 @@ def numbers( if (min_int is not None) and (max_int is not None): _ = assume(min_int <= max_int) st_integers = integers(min_int, max_int) + if ( + (min_value_ is not None) + and isclose(min_value_, 0.0) + and (max_value_ is not None) + and isclose(max_value_, 0.0) + ): + min_value_ = max_value_ = 0.0 st_floats = floats( min_value=min_value_, max_value=max_value_, @@ -938,6 +957,27 @@ def timedeltas_2w( ## +def triples( + strategy: SearchStrategy[_T], + /, + *, + unique: MaybeSearchStrategy[bool] = False, + sorted: MaybeSearchStrategy[bool] = False, # noqa: A002 +) -> SearchStrategy[tuple[_T, _T, _T]]: + """Strategy for generating triples of elements.""" + return lists_fixed_length(strategy, 3, unique=unique, sorted=sorted).map( + _triples_map + ) + + +def _triples_map(elements: list[_T], /) -> tuple[_T, _T, _T]: + first, second, third = elements + return first, second, third + + +## + + @composite def uint32s( _draw: DrawFn, @@ -973,15 +1013,14 @@ def uint64s( ## -def versions() -> SearchStrategy[Version]: +@composite +def versions(_draw: DrawFn, /, *, suffix: MaybeSearchStrategy[bool] = False) -> Version: """Strategy for generating versions.""" - return builds( - Version, - major=integers(min_value=0), - minor=integers(min_value=0), - patch=integers(min_value=0), - suffix=text_ascii(min_size=1) | none(), - ) + draw = lift_draw(_draw) + major, minor, patch = draw(triples(integers(min_value=0))) + _ = assume((major >= 1) or (minor >= 1) or (patch >= 1)) + suffix_use = draw(text_ascii(min_size=1)) if draw(suffix) else None + return Version(major=major, minor=minor, patch=patch, suffix=suffix_use) ## @@ -1104,6 +1143,7 @@ def zoned_datetimes( "text_digits", "text_printable", "timedeltas_2w", + "triples", "uint32s", "uint64s", "versions", diff --git a/src/utilities/logging.py b/src/utilities/logging.py index 2ac733838..d001562ef 100644 --- a/src/utilities/logging.py +++ b/src/utilities/logging.py @@ -26,7 +26,7 @@ from utilities.atomicwrites import writer from utilities.datetime import get_now, maybe_sub_pct_y -from utilities.git import get_repo_root +from utilities.git import MASTER, get_repo_root from utilities.pathlib import ensure_suffix, resolve_path from utilities.traceback import RichTracebackFormatter from utilities.types import LogLevel @@ -131,6 +131,7 @@ def setup_logging( console_level: LogLevel | None = "INFO", console_filters: Iterable[_FilterType] | None = None, console_fmt: str = "❯ {_zoned_datetime_str} | {name}:{funcName}:{lineno} | {message}", # noqa: RUF001 + git_ref: str = MASTER, files_dir: PathLikeOrCallable | None = get_default_logging_path, files_when: str = "D", files_interval: int = 1, @@ -201,7 +202,7 @@ class LogRecordNanoLocal( # skipif-ci-and-windows add_filters(console_high_handler, filters=console_filters) add_filters(console_high_handler, filters=filters) _ = RichTracebackFormatter.create_and_set( - console_high_handler, detail=True, post=_ansi_wrap_red + console_high_handler, git_ref=git_ref, detail=True, post=_ansi_wrap_red ) console_high_handler.setLevel( max(get_logging_level_number(console_level), ERROR) @@ -244,7 +245,9 @@ class LogRecordNanoLocal( # skipif-ci-and-windows level=ERROR, path=directory.joinpath("errors") ) add_filters(standalone_file_handler, filters=[lambda x: x.exc_info is not None]) - standalone_file_handler.setFormatter(RichTracebackFormatter(detail=True)) + standalone_file_handler.setFormatter( + RichTracebackFormatter(git_ref=git_ref, detail=True) + ) logger_use.addHandler(standalone_file_handler) # skipif-ci-and-windows # extra diff --git a/src/utilities/traceback.py b/src/utilities/traceback.py index 94c4f7ce3..294a77de8 100644 --- a/src/utilities/traceback.py +++ b/src/utilities/traceback.py @@ -37,6 +37,7 @@ get_func_name, get_func_qualname, ) +from utilities.git import MASTER from utilities.iterables import one from utilities.rich import ( EXPAND_ALL, @@ -79,10 +80,12 @@ def __init__( /, *, defaults: StrMapping | None = None, + git_ref: str = MASTER, detail: bool = False, post: Callable[[str], str] | None = None, ) -> None: super().__init__(fmt, datefmt, style, validate, defaults=defaults) + self._git_ref = git_ref self._detail = detail self._post = post @@ -92,7 +95,7 @@ def format(self, record: LogRecord) -> str: if record.exc_info is None: return f"ERROR: {record.exc_info=}" _, exc_value, _ = record.exc_info - error = get_rich_traceback(ensure_not_none(exc_value)) + error = get_rich_traceback(ensure_not_none(exc_value), git_ref=self._git_ref) match error: case ExcChainTB() | ExcGroupTB() | ExcTB(): text = error.format(header=True, detail=self._detail) @@ -115,12 +118,20 @@ def create_and_set( style: _FormatStyle = "%", validate: bool = True, defaults: StrMapping | None = None, + git_ref: str, detail: bool = False, post: Callable[[str], str] | None = None, ) -> Self: """Create an instance and set it on a handler.""" formatter = cls( - fmt, datefmt, style, validate, defaults=defaults, detail=detail, post=post + fmt, + datefmt, + style, + validate, + defaults=defaults, + git_ref=git_ref, + detail=detail, + post=post, ) handler.addFilter(lambda r: r.exc_info is not None) handler.setFormatter(formatter) @@ -211,6 +222,7 @@ class ExcChainTB(Generic[_TExc]): errors: list[ExcGroupTB[_TExc] | ExcTB[_TExc] | BaseException] = field( default_factory=list ) + git_ref: str = field(default=MASTER, repr=False) def __iter__(self) -> Iterator[ExcGroupTB[_TExc] | ExcTB[_TExc] | BaseException]: yield from self.errors @@ -237,7 +249,7 @@ def format( """Format the traceback.""" lines: list[str] = [] if header: # pragma: no cover - lines.extend(_yield_header_lines()) + lines.extend(_yield_header_lines(git_ref=self.git_ref)) total = len(self.errors) for i, errors in enumerate(self.errors): lines.append(f"Exception chain {i + 1}/{total}:") @@ -288,6 +300,7 @@ class ExcGroupTB(Generic[_TExc]): errors: list[ExcGroupTB[_TExc] | ExcTB[_TExc] | BaseException] = field( default_factory=list ) + git_ref: str = field(default=MASTER, repr=False) @override def __repr__(self) -> str: @@ -311,7 +324,7 @@ def format( """Format the traceback.""" lines: list[str] = [] if header: # pragma: no cover - lines.extend(_yield_header_lines()) + lines.extend(_yield_header_lines(git_ref=self.git_ref)) lines.extend([ f"Exception group {index + 1}/{total}:", indent("Path:", _INDENT), @@ -350,6 +363,7 @@ class ExcTB(Generic[_TExc]): frames: list[_Frame] = field(default_factory=list) error: _TExc + git_ref: str = field(default=MASTER, repr=False) def __iter__(self) -> Iterator[_Frame]: yield from self.frames @@ -378,7 +392,7 @@ def format( total = len(self) lines: list[str] = [] if header: # pragma: no cover - lines.extend(_yield_header_lines()) + lines.extend(_yield_header_lines(git_ref=self.git_ref)) for i, frame in enumerate(self.frames): is_head = i < total - 1 lines.append( @@ -472,7 +486,7 @@ def format( def get_rich_traceback( - error: _TExc, / + error: _TExc, /, *, git_ref: str = MASTER ) -> ExcChainTB[_TExc] | ExcGroupTB[_TExc] | ExcTB[_TExc] | BaseException: """Get a rich traceback.""" match list(yield_exceptions(error)): @@ -480,26 +494,33 @@ def get_rich_traceback( raise ImpossibleCaseError(case=[f"{error}"]) case [err]: err = cast(_TExc, err) - return _get_rich_traceback_non_chain(err) + return _get_rich_traceback_non_chain(err, git_ref=git_ref) case errors: errors = cast(list[_TExc], errors) - return ExcChainTB(errors=[_get_rich_traceback_non_chain(e) for e in errors]) + return ExcChainTB( + errors=[ + _get_rich_traceback_non_chain(e, git_ref=git_ref) for e in errors + ], + git_ref=git_ref, + ) def _get_rich_traceback_non_chain( - error: _TExc, / + error: _TExc, /, *, git_ref: str = MASTER ) -> ExcTB[_TExc] | ExcGroupTB[_TExc] | BaseException: """Get a rich traceback, for a non-chained error.""" if not isinstance(error, ExceptionGroup): - return _get_rich_traceback_non_chain_non_group(error) - path = cast(ExcTB[_TExc], _get_rich_traceback_non_chain_non_group(error)) + return _get_rich_traceback_non_chain_non_group(error, git_ref=git_ref) + path = cast( + ExcTB[_TExc], _get_rich_traceback_non_chain_non_group(error, git_ref=git_ref) + ) errors = cast(list[_TExc], error.exceptions) - errors = list(map(_get_rich_traceback_non_chain, errors)) - return ExcGroupTB(path=path, errors=errors) + errors = [_get_rich_traceback_non_chain(e, git_ref=git_ref) for e in errors] + return ExcGroupTB(path=path, errors=errors, git_ref=git_ref) def _get_rich_traceback_non_chain_non_group( - error: _TExc, / + error: _TExc, /, *, git_ref: str = MASTER ) -> ExcTB[_TExc] | BaseException: """Get a rich traceback, for a non-chained, non-grouped error.""" if isinstance(error, _HasExceptionPath): @@ -515,7 +536,7 @@ def _get_rich_traceback_non_chain_non_group( ) for f in error.exc_tb.frames ] - return ExcTB(frames=frames, error=cast(_TExc, error)) + return ExcTB(frames=frames, error=cast(_TExc, error), git_ref=git_ref) return error @@ -714,12 +735,12 @@ def is_match(curr: _ExtFrameSummaryCAOpt, next_: _ExtFrameSummaryCA, /) -> bool: return values[::-1] -def _yield_header_lines() -> Iterator[str]: +def _yield_header_lines(*, git_ref: str = MASTER) -> Iterator[str]: """Yield the header lines.""" yield f"Date/time | {serialize_zoned_datetime(get_now(time_zone='local'))}" yield f"User | {getuser()}" yield f"Host | {gethostname()}" - yield f"Version | {get_version()}" + yield f"Version | {get_version(ref=git_ref)}" yield "" diff --git a/src/utilities/version.py b/src/utilities/version.py index a17057abd..d5f6e5d9c 100644 --- a/src/utilities/version.py +++ b/src/utilities/version.py @@ -1,13 +1,14 @@ from __future__ import annotations import re -from dataclasses import dataclass, replace +from dataclasses import dataclass, field, replace +from functools import total_ordering from subprocess import check_output -from typing import TYPE_CHECKING, Self +from typing import TYPE_CHECKING, Any, Self from typing_extensions import override -from utilities.git import fetch_all_tags, get_ref_tags +from utilities.git import MASTER, fetch_all_tags, get_ref_tags from utilities.iterables import one from utilities.pathlib import PWD @@ -18,16 +19,24 @@ _PATTERN = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:-(\w+))?") -@dataclass(repr=False, order=True, frozen=True, kw_only=True, slots=True) +## + + +@dataclass(repr=False, frozen=True, slots=True) +@total_ordering class Version: """A version identifier.""" major: int = 0 - minor: int = 1 - patch: int = 0 - suffix: str | None = None + minor: int = 0 + patch: int = 1 + suffix: str | None = field(default=None, kw_only=True) def __post_init__(self) -> None: + if (self.major == 0) and (self.minor == 0) and (self.patch == 0): + raise _VersionZeroError( + major=self.major, minor=self.minor, patch=self.patch + ) if self.major < 0: raise _VersionNegativeMajorVersionError(major=self.major) if self.minor < 0: @@ -37,6 +46,23 @@ def __post_init__(self) -> None: if (self.suffix is not None) and (len(self.suffix) == 0): raise _VersionEmptySuffixError(suffix=self.suffix) + def __le__(self, other: Any, /) -> bool: + if not isinstance(other, type(self)): + return NotImplemented + self_as_tuple = ( + self.major, + self.minor, + self.patch, + "" if self.suffix is None else self.suffix, + ) + other_as_tuple = ( + other.major, + other.minor, + other.patch, + "" if other.suffix is None else other.suffix, + ) + return self_as_tuple <= other_as_tuple + @override def __repr__(self) -> str: version = f"{self.major}.{self.minor}.{self.patch}" @@ -45,13 +71,13 @@ def __repr__(self) -> str: return version def bump_major(self) -> Self: - return type(self)(major=self.major + 1, minor=0, patch=0) + return type(self)(self.major + 1, 0, 0) def bump_minor(self) -> Self: - return type(self)(major=self.major, minor=self.minor + 1, patch=0) + return type(self)(self.major, self.minor + 1, 0) def bump_patch(self) -> Self: - return type(self)(major=self.major, minor=self.minor, patch=self.patch + 1) + return type(self)(self.major, self.minor, self.patch + 1) def with_suffix(self, *, suffix: str | None = None) -> Self: return replace(self, suffix=suffix) @@ -61,6 +87,17 @@ def with_suffix(self, *, suffix: str | None = None) -> Self: class VersionError(Exception): ... +@dataclass(kw_only=True, slots=True) +class _VersionZeroError(VersionError): + major: int + minor: int + patch: int + + @override + def __str__(self) -> str: + return f"Version must be greater than zero; got {self.major}.{self.minor}.{self.patch}" + + @dataclass(kw_only=True, slots=True) class _VersionNegativeMajorVersionError(VersionError): major: int @@ -97,33 +134,40 @@ def __str__(self) -> str: return f"Suffix must be non-empty; got {self.suffix!r}" -def get_git_version(*, cwd: PathLike = PWD) -> Version: +## + + +def get_git_version(*, cwd: PathLike = PWD, ref: str = MASTER) -> Version: """Get the version according to the `git`.""" fetch_all_tags(cwd=cwd) - tags = get_ref_tags("origin/master", cwd=cwd) + tags = get_ref_tags(ref, cwd=cwd) tag = one(tags) return parse_version(tag) +## + + def get_hatch_version(*, cwd: PathLike = PWD) -> Version: """Get the version according to `hatch`.""" output = check_output(["hatch", "version"], cwd=cwd, text=True) return parse_version(output.strip("\n")) -def get_version(*, cwd: PathLike = PWD) -> Version: +## + + +def get_version(*, cwd: PathLike = PWD, ref: str = MASTER) -> Version: """Get the version.""" - git = get_git_version(cwd=cwd) + git = get_git_version(cwd=cwd, ref=ref) hatch = get_hatch_version(cwd=cwd) - if hatch == git: # pragma: no cover + if hatch < git: + return hatch.with_suffix(suffix="behind") + if hatch == git: return hatch - if hatch in { # pragma: no cover - git.bump_major(), - git.bump_minor(), - git.bump_patch(), - }: + if hatch in {git.bump_major(), git.bump_minor(), git.bump_patch()}: return hatch.with_suffix(suffix="dirty") - raise GetVersionError(git=git, hatch=hatch) # pragma: no cover + raise GetVersionError(git=git, hatch=hatch) @dataclass(kw_only=True, slots=True) @@ -133,7 +177,10 @@ class GetVersionError(Exception): @override def __str__(self) -> str: - return f"`git` and `hatch` versions are incompatible; got {self.git} and {self.hatch}" # pragma: no cover + return f"`hatch` version is ahead of `git` version in an incompatible way; got {self.hatch} and {self.git}" + + +## def parse_version(version: str, /) -> Version: @@ -142,9 +189,7 @@ def parse_version(version: str, /) -> Version: if not result: raise ParseVersionError(version=version) major_str, minor_str, patch_str, suffix = result.groups() - return Version( - major=int(major_str), minor=int(minor_str), patch=int(patch_str), suffix=suffix - ) + return Version(int(major_str), int(minor_str), int(patch_str), suffix=suffix) @dataclass(kw_only=True, slots=True)