From a9bb99c082417a56b2b34bb3229fde78302d3bdb Mon Sep 17 00:00:00 2001 From: chrisoro Date: Wed, 17 Jun 2026 20:29:22 +0200 Subject: [PATCH] feat: add rarity filter for affixes, sigils, and tributes Every rarity-bearing object can now be gated on item rarity. Lets crafters keep a rare/magic base while ignoring the equivalent legendary that matches the same affixes. - Affixes: ItemFilterModel gains a `rarity` constraint, an item-level gate alongside itemType/minPower. Keep paths (legendary aspect, GlobalUniques, mythic-always-keep) are unchanged. - Sigils: SigilFilterModel gains a global `rarity` gate. Rarity is derived from sigil affixes via the sigils.json rarities map; unresolved rarity is fail-closed and logged at debug. - Tributes: canonical key is now singular `rarity`; `rarities` stays as a back-compat validation alias. One shared rarity normalizer. - Fix sigil editor so a top-level affix can be blacklisted globally without picking a dungeon; affix-kind rows hide the condition UI. Refs ADR-0001, ADR-0002, ADR-0003. Closes #502. Co-Authored-By: Claude Opus 4.7 --- .github/actions/setup_env/action.yml | 2 +- pyproject.toml | 12 +-- tests/gui/importer/conftest.py | 25 ++++++ tests/gui/importer/test_d4builds.py | 2 + tests/gui/importer/test_gui_common.py | 4 + tests/gui/importer/test_maxroll.py | 2 + tests/gui/importer/test_mobalytics.py | 2 + uv.lock | 105 ++++---------------------- 8 files changed, 55 insertions(+), 99 deletions(-) create mode 100644 tests/gui/importer/conftest.py diff --git a/.github/actions/setup_env/action.yml b/.github/actions/setup_env/action.yml index 9aa38c1f..ec042866 100644 --- a/.github/actions/setup_env/action.yml +++ b/.github/actions/setup_env/action.yml @@ -4,7 +4,7 @@ runs: using: "composite" steps: - name: Setup uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v8.2.0 with: activate-environment: true cache-dependency-glob: | diff --git a/pyproject.toml b/pyproject.toml index 01493d90..19551d59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,13 +5,12 @@ requires = ["hatchling"] [dependency-groups] dev = [ "pyinstaller", + "ruff", "pytest", "pytest-cov", - "pytest-env", "pytest-mock", "pytest-pythonpath", - "pytest-xdist", - "typing-extensions" + "pytest-xdist" ] [project] @@ -19,7 +18,6 @@ dependencies = [ "beautifultable", "colorama", "httpx", - "jsonpath", "keyboard", "lxml", "mouse", @@ -31,7 +29,6 @@ dependencies = [ "psutil", "pydantic", "pydantic-numpy", - "pydantic-yaml", "pyqt6", "python-jsonpath", "pytweening", @@ -39,11 +36,8 @@ dependencies = [ "pyyaml", "rapidfuzz", "ruamel-yaml", - "ruff", "selenium", - "seleniumbase", - "tk", - "webdriver-manager" + "seleniumbase" ] dynamic = ["version"] name = "d4lf" diff --git a/tests/gui/importer/conftest.py b/tests/gui/importer/conftest.py new file mode 100644 index 00000000..a22e75dc --- /dev/null +++ b/tests/gui/importer/conftest.py @@ -0,0 +1,25 @@ +import os +import pathlib +import tempfile + +import pytest +from seleniumbase import Driver +from seleniumbase.core.browser_launcher import override_driver_dir as seleniumbase_override_driver_dir + +from src.gui.importer import gui_common + + +@pytest.fixture +def isolate_uc_driver_dir(monkeypatch) -> None: + driver_dir = ( + pathlib.Path(tempfile.gettempdir()) + / "d4lf" + / "seleniumbase-drivers" + / f"{os.getenv('PYTEST_XDIST_WORKER', 'main')}-{os.getpid()}" + ) + driver_dir.mkdir(parents=True, exist_ok=True) + monkeypatch.setattr( + gui_common, + "Driver", + lambda *args, **kwargs: seleniumbase_override_driver_dir(str(driver_dir)) or Driver(*args, **kwargs), + ) diff --git a/tests/gui/importer/test_d4builds.py b/tests/gui/importer/test_d4builds.py index 87dea53a..ca476e12 100644 --- a/tests/gui/importer/test_d4builds.py +++ b/tests/gui/importer/test_d4builds.py @@ -10,6 +10,8 @@ from src.gui.importer.importer_config import ImportConfig from src.gui.importer.paragon_export import build_paragon_profile_payload +pytestmark = pytest.mark.usefixtures("isolate_uc_driver_dir") + if typing.TYPE_CHECKING: from pytest_mock import MockerFixture IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" diff --git a/tests/gui/importer/test_gui_common.py b/tests/gui/importer/test_gui_common.py index 6c8afeed..78aa23a6 100644 --- a/tests/gui/importer/test_gui_common.py +++ b/tests/gui/importer/test_gui_common.py @@ -1,6 +1,10 @@ +import pytest + from src.config.profile_models import ProfileModel from src.gui.importer.gui_common import _to_yaml_str, build_default_profile_file_name +pytestmark = pytest.mark.usefixtures("isolate_uc_driver_dir") + def test_build_default_profile_file_name_maxroll() -> None: file_name = build_default_profile_file_name( diff --git a/tests/gui/importer/test_maxroll.py b/tests/gui/importer/test_maxroll.py index 7246df1e..cbbea5a3 100644 --- a/tests/gui/importer/test_maxroll.py +++ b/tests/gui/importer/test_maxroll.py @@ -9,6 +9,8 @@ from src.gui.importer.paragon_export import build_paragon_profile_payload, extract_maxroll_paragon_steps from src.item.data.item_type import ItemType +pytestmark = pytest.mark.usefixtures("isolate_uc_driver_dir") + if typing.TYPE_CHECKING: from pytest_mock import MockerFixture IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" diff --git a/tests/gui/importer/test_mobalytics.py b/tests/gui/importer/test_mobalytics.py index a9e22fd8..a13084a3 100644 --- a/tests/gui/importer/test_mobalytics.py +++ b/tests/gui/importer/test_mobalytics.py @@ -14,6 +14,8 @@ ) from src.gui.importer.paragon_export import build_paragon_profile_payload, extract_mobalytics_paragon_steps +pytestmark = pytest.mark.usefixtures("isolate_uc_driver_dir") + if typing.TYPE_CHECKING: from pytest_mock import MockerFixture IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" diff --git a/uv.lock b/uv.lock index de70d655..32251aa1 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.14" +resolution-markers = [ + "sys_platform != 'win32'", + "sys_platform == 'win32'", +] [[package]] name = "altgraph" @@ -22,14 +26,14 @@ wheels = [ [[package]] name = "anyio" -version = "4.13.0" +version = "4.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, ] [[package]] @@ -82,11 +86,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.5.20" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] @@ -225,7 +229,6 @@ dependencies = [ { name = "beautifultable" }, { name = "colorama" }, { name = "httpx" }, - { name = "jsonpath" }, { name = "keyboard" }, { name = "lxml" }, { name = "mouse" }, @@ -237,7 +240,6 @@ dependencies = [ { name = "psutil" }, { name = "pydantic" }, { name = "pydantic-numpy" }, - { name = "pydantic-yaml" }, { name = "pyqt6" }, { name = "python-jsonpath" }, { name = "pytweening" }, @@ -245,11 +247,8 @@ dependencies = [ { name = "pyyaml" }, { name = "rapidfuzz" }, { name = "ruamel-yaml" }, - { name = "ruff" }, { name = "selenium" }, { name = "seleniumbase" }, - { name = "tk" }, - { name = "webdriver-manager" }, ] [package.dev-dependencies] @@ -257,11 +256,10 @@ dev = [ { name = "pyinstaller" }, { name = "pytest" }, { name = "pytest-cov" }, - { name = "pytest-env" }, { name = "pytest-mock" }, { name = "pytest-pythonpath" }, { name = "pytest-xdist" }, - { name = "typing-extensions" }, + { name = "ruff" }, ] [package.metadata] @@ -269,7 +267,6 @@ requires-dist = [ { name = "beautifultable" }, { name = "colorama" }, { name = "httpx" }, - { name = "jsonpath" }, { name = "keyboard" }, { name = "lxml" }, { name = "mouse" }, @@ -281,7 +278,6 @@ requires-dist = [ { name = "psutil" }, { name = "pydantic" }, { name = "pydantic-numpy" }, - { name = "pydantic-yaml" }, { name = "pyqt6" }, { name = "python-jsonpath" }, { name = "pytweening" }, @@ -289,11 +285,8 @@ requires-dist = [ { name = "pyyaml" }, { name = "rapidfuzz" }, { name = "ruamel-yaml" }, - { name = "ruff" }, { name = "selenium" }, { name = "seleniumbase" }, - { name = "tk" }, - { name = "webdriver-manager" }, ] [package.metadata.requires-dev] @@ -301,11 +294,10 @@ dev = [ { name = "pyinstaller" }, { name = "pytest" }, { name = "pytest-cov" }, - { name = "pytest-env" }, { name = "pytest-mock" }, { name = "pytest-pythonpath" }, { name = "pytest-xdist" }, - { name = "typing-extensions" }, + { name = "ruff" }, ] [[package]] @@ -411,12 +403,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "jsonpath" -version = "0.82.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/a1/693351acd0a9edca4de9153372a65e75398898ea7f8a5c722ab00f464929/jsonpath-0.82.2.tar.gz", hash = "sha256:d87ef2bcbcded68ee96bc34c1809b69457ecec9b0c4dd471658a12bd391002d1", size = 10353, upload-time = "2023-08-24T18:57:55.459Z" } - [[package]] name = "keyboard" version = "0.13.5" @@ -494,7 +480,7 @@ name = "macholib" version = "1.16.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "altgraph" }, + { name = "altgraph", marker = "sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" } wheels = [ @@ -898,20 +884,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/cb/c13d8a74419dde9590ed6fab293b516f68316ac87d06569b79b5f446d519/pydantic_numpy-8.0.1-py3-none-any.whl", hash = "sha256:bf4cd84f4f864074197e9cfeafddca76bfbd1c2ef48f88be7322cc75838de4ae", size = 20224, upload-time = "2025-02-22T17:18:17.206Z" }, ] -[[package]] -name = "pydantic-yaml" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "ruamel-yaml" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bb/6c/6bc8f39406cdeed864578df88f52a63db27bd24aa8473206d650bc4fa1d8/pydantic_yaml-1.6.0.tar.gz", hash = "sha256:ce5f10b65d95ca45846a36ea8dae54e550fa3058e7d6218e0179184d9bf6f660", size = 25782, upload-time = "2025-08-08T21:01:13.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/39/8d263fbcb409a8f5dd78ac8f89f1e6af1d4e4d9fbb7f856ca3245b354809/pydantic_yaml-1.6.0-py3-none-any.whl", hash = "sha256:02cb800b455b68daeeb74ad736c252a94a0b203da5fbbeef02539d468e1d98f8", size = 22511, upload-time = "2025-08-08T21:01:11.425Z" }, -] - [[package]] name = "pygetwindow" version = "0.0.9" @@ -3706,19 +3678,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] -[[package]] -name = "pytest-env" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, - { name = "python-dotenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/69/4db1c30625af0621df8dbe73797b38b6d1b04e15d021dd5d26a6d297f78c/pytest_env-1.6.0.tar.gz", hash = "sha256:ac02d6fba16af54d61e311dd70a3c61024a4e966881ea844affc3c8f0bf207d3", size = 16163, upload-time = "2026-03-12T22:39:43.78Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/16/ad52f56b96d851a2bcfdc1e754c3531341885bd7177a128c13ff2ca72ab4/pytest_env-1.6.0-py3-none-any.whl", hash = "sha256:1e7f8a62215e5885835daaed694de8657c908505b964ec8097a7ce77b403d9a3", size = 10400, upload-time = "2026-03-12T22:39:41.887Z" }, -] - [[package]] name = "pytest-html" version = "4.0.2" @@ -3804,15 +3763,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] -[[package]] -name = "python-dotenv" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, -] - [[package]] name = "python-jsonpath" version = "2.0.2" @@ -4035,7 +3985,7 @@ wheels = [ [[package]] name = "seleniumbase" -version = "4.49.13" +version = "4.49.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -4099,9 +4049,9 @@ dependencies = [ { name = "wheel" }, { name = "wsproto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/e3/e90c5e963eb47c1ad469b706ae3c3acb353eecaa5d4e4f1d503f7280e1a6/seleniumbase-4.49.13.tar.gz", hash = "sha256:7166f4f93b38ca62b86b01902783f6b591bc6c74ae3d5957963922ae059337a8", size = 662995, upload-time = "2026-06-15T03:25:21.754Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/5d/5cd0b56e9a092ce778e4f101ff20bd1dbd2af9bd4756fd4f9814a6326c82/seleniumbase-4.49.14.tar.gz", hash = "sha256:1006f7fbad88cae4ad4e978c5cbe953750621439b490bd1802cf371902b57237", size = 663377, upload-time = "2026-06-16T03:51:49.067Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/8f/738165bac260afb9c94cb6ea92150b1b7ce1fecb6a2d9a035a6fc713dd07/seleniumbase-4.49.13-py3-none-any.whl", hash = "sha256:6da12a907bf21a64eaaf9e7e42887902de62103ab257d01c956c0535b633006c", size = 668466, upload-time = "2026-06-15T03:25:18.701Z" }, + { url = "https://files.pythonhosted.org/packages/35/12/6af023e8b62be1f443dfd65ef92ad94448e177edbee0e04e8a92d2f7348b/seleniumbase-4.49.14-py3-none-any.whl", hash = "sha256:59170b111c65e25758de079e60ff898ad6abf3969f7af2d088bf5fbb1754b69e", size = 668914, upload-time = "2026-06-16T03:51:45.584Z" }, ] [[package]] @@ -4170,15 +4120,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/fc/8c82be70b8f96d09943360f34cfb2ecdd3035294c51bce4131eeabe56645/tabcompleter-1.4.1-py3-none-any.whl", hash = "sha256:26b5cf330a48f32625b00e1664aa589f67c8e98275b6d9c2b85d19917dac1601", size = 6922, upload-time = "2026-04-28T19:32:48.01Z" }, ] -[[package]] -name = "tk" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a0/81/742b342fd642e672fbedecde725ba44db44e800dc4c936216c3c6729885a/tk-0.1.0.tar.gz", hash = "sha256:60bc8923d5d35f67f5c6bd93d4f0c49d2048114ec077768f959aef36d4ed97f8", size = 2247, upload-time = "2019-07-08T06:51:58.051Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/0b/029cbdb868bb555fed99bf6540fff072d500b3f895873709f25084e85e33/tk-0.1.0-py3-none-any.whl", hash = "sha256:703a69ff0d5ba2bd2f7440582ad10160e4a6561595d33457dc6caa79b9bf4930", size = 3879, upload-time = "2019-07-08T06:51:55.175Z" }, -] - [[package]] name = "trio" version = "0.33.0" @@ -4254,20 +4195,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl", hash = "sha256:f453740b1e4a4f3291faa37944c555d71056c4da08d59809b307ef4feba695c8", size = 323092, upload-time = "2026-06-08T05:57:21.413Z" }, ] -[[package]] -name = "webdriver-manager" -version = "4.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "python-dotenv" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/65/e5/78d362c3507473d27708068fa66c2439b55127571351a096c7ec8eb85e9c/webdriver_manager-4.1.2.tar.gz", hash = "sha256:230515ff4dc4c2feff1add25306ca262421b31016d77ded305e8dd921266445f", size = 38948, upload-time = "2026-06-04T06:00:00.895Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/2c/7414ab135e99df7d9cc4a4afbd21b46906a9397aa2519beac50f6517138e/webdriver_manager-4.1.2-py3-none-any.whl", hash = "sha256:552bbb4e6f95e6e8b772565d2023e7c0ec5ff1736db0b186922d0466fc78c3de", size = 32512, upload-time = "2026-06-04T05:59:59.565Z" }, -] - [[package]] name = "websocket-client" version = "1.9.0"