diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 25536d4a..2c6704bd 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -26,7 +26,18 @@ jobs: - name: Install ruff run: python -m pip install ruff==0.16.9 + # Scope is explicit rather than `.` so the docstring baseline in + # pyproject.toml covers exactly these paths, and so generated or vendored + # trees cannot silently widen what CI lints. `test/` is included because + # the per-file-ignores baseline lists tiers for it; without it those tiers + # are dead configuration. - name: Run tests and linters run: | - poetry run ruff check photoshop - poetry run ruff format --check photoshop + poetry run ruff check photoshop test scripts + poetry run ruff format --check photoshop test scripts + + # llms.txt and llms-full.txt are generated. The Release workflow opens a + # pull request when they drift, but that only reports — this is what makes + # a stale file fail the build. + - name: Check generated llms files are current + run: python scripts/generate_llms.py --check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8d0d114..71d342d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -177,14 +177,22 @@ jobs: # ── llms.txt / llms-full.txt — regenerate before the release is cut ─────── # Generated files, never hand-edited. Runs ahead of release-please so the - # version it stamps into llms.txt is the one about to be released. When the - # result differs from what is committed it opens (or updates) its own PR - # instead of pushing to main, so the content is reviewable like any other - # change and never silently rewrites the release branch. + # files are up to date on the branch the release is cut from. When the result + # differs from what is committed it opens (or updates) its own PR instead of + # pushing to main, so the content is reviewable like any other change and + # never silently rewrites the release branch. + # + # The version stamped into llms.txt is the last *released* one: the generator + # reads the newest tag with `git describe --tags --abbrev=0`, and no tag for + # the pending release exists yet this early in the run. So each release is + # followed by one docs PR carrying the new version — harmless, because + # preflight treats `docs:` as release-neutral and does not cut a release for + # it. # # Non-blocking by design: `release-please` uses `always()` below, so a failure # here reports red but does not hold the release hostage. The ordering is what - # matters, not the gate. + # matters, not the gate. The `Python package` workflow is what makes stale + # generated files fail the build. llms-txt: needs: [preflight] runs-on: ubuntu-latest diff --git a/llms.txt b/llms.txt index 3e653f39..dfea95b2 100644 --- a/llms.txt +++ b/llms.txt @@ -58,3 +58,5 @@ with Session(action='new_document') as ps: - [Changelog](https://github.com/loonghao/photoshop-python-api/blob/main/CHANGELOG.md) — release history. - [Issue tracker](https://github.com/loonghao/photoshop-python-api/issues) — known problems and questions. + +drift injected diff --git a/pyproject.toml b/pyproject.toml index 2679a128..1ac818a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,24 +92,41 @@ extend-exclude = [ # in CI, so only pycodestyle/pyflakes rules ever fired. Quote rules are enabled # here because the codebase already satisfies them. # -# The docstring rules (D) are enabled but baselined per directory below: every -# finding that exists today is recorded in `per-file-ignores`, so CI stays green -# on the current tree while any NEW docstring debt is caught immediately. The -# baseline is meant to shrink — pick a tier, fix it, delete its entry. +# The docstring rules (D) are enabled, with the debt that existed when they were +# switched on recorded per directory below. See the baseline block for what that +# does and does not catch — the short version is that a rule listed for a file +# stays silent for that file, including new violations of it. select = ["E", "W", "F", "Q", "I", "UP", "D"] -# Selecting the whole D family makes ruff drop the mutually exclusive half of two -# pairs on its own: D203 loses to D211, and D213 loses to D212. Both are reported -# as warnings at startup, which is expected and not something to "fix". +# The google convention selects a narrower set of pydocstyle rules than the +# default (pep257): 665 findings tree-wide under google versus 864 under pep257, +# 507 of them inside the CI lint scope. It also resolves the two mutually +# exclusive rule pairs (D203/D211 and D212/D213), so ruff emits no warnings. [tool.ruff.lint.pydocstyle] convention = "google" # ── Docstring baseline ────────────────────────────────────────────────────── -# 864 findings existed when D was switched on. Each entry is a directory tier -# plus the rules that tier currently violates; the number is the finding count -# at baseline time. Deleting a rule from an entry makes that tier's existing -# debt fail CI until it is fixed, which is the intended ratchet. +# 665 findings existed when D was switched on (507 inside the CI lint scope). +# Each entry is a directory tier plus the rules that tier currently violates; +# the number is the finding count at baseline time. +# +# Read this before touching anything here. `per-file-ignores` is a UNION of +# every pattern that matches a file, not a "most specific pattern wins" lookup: +# +# - A rule listed for a file is ignored for that file, so NEW violations of +# that same rule are also ignored. This baseline stops the tree from going +# red; it does not stop new debt in already-baselined rules. Only violations +# of rules that are NOT listed for a file get caught. +# - Deleting a rule from an entry only tightens anything when no other +# matching entry still lists it. Every file under photoshop/ also matches +# "photoshop/**", so removing a rule from a narrower tier while the widest +# tier still lists it is a no-op. Tighten a rule by removing it from the +# widest tier that covers the files you just fixed. [tool.ruff.lint.per-file-ignores] +# Ordered narrowest to widest, but order carries no meaning to ruff: matching +# entries are unioned, so the widest entry below is the effective floor for +# everything under photoshop/. +# # Enum constant tables: one IntEnum class per Photoshop constant, 129 findings. "photoshop/api/enumerations.py" = ["D101"] # Save-option wrappers, 112 findings. @@ -124,7 +141,9 @@ convention = "google" "test/**" = ["D100", "D101", "D102", "D103", "D104", "D105", "D107", "D202", "D401", "D403"] # The COM wrapper API, 443 findings. "photoshop/api/**" = ["D100", "D101", "D102", "D105", "D107", "D205", "D209", "D212", "D214", "D301", "D400", "D401", "D403", "D404", "D405", "D413", "D414", "D415", "D417"] -# Package root, 8 findings. +# Package root, 8 findings. Effective floor for the whole package: a rule listed +# here is ignored for every file under photoshop/, whichever tier above also +# lists it. Remove a rule here once no file under photoshop/ violates it. "photoshop/**" = ["D100", "D102", "D104", "D105", "D205", "D401", "D403"] [tool.ruff.lint.flake8-quotes] diff --git a/scripts/generate_llms.py b/scripts/generate_llms.py index bef90566..310e4cf6 100644 --- a/scripts/generate_llms.py +++ b/scripts/generate_llms.py @@ -5,15 +5,16 @@ inline so a model can consume it in one request without fetching anything. Both files are generated, never hand-edited. The ``llms-txt`` job in -``.github/workflows/llms-txt.yml`` regenerates them and opens a pull request +``.github/workflows/release.yml`` regenerates them and opens a pull request whenever the result differs from what is committed. Run it locally with:: python scripts/generate_llms.py -Pass ``--check`` to exit non-zero when the committed files are stale, which -is how the CI job decides whether a pull request is needed. +Pass ``--check`` to exit non-zero when the committed files are stale. The +``Python package`` workflow runs that on every pull request, so drift fails +the build; the ``llms-txt`` job only opens a pull request to report it. """ # Import future modules @@ -175,7 +176,14 @@ def public_modules() -> list[str]: def version() -> str: - """Return the current package version from git, falling back to pyproject.""" + """Return the newest released version, falling back to pyproject. + + This reads the most recent tag, which is the version that has *already* + been published rather than the one a pending release is about to cut: the + ``llms-txt`` job runs ahead of release-please, so no tag for the upcoming + version exists yet. The generated files therefore carry the last released + version until the release lands and the job runs again. + """ try: result = subprocess.run( ["git", "describe", "--tags", "--abbrev=0"], diff --git a/test/__init__.py b/test/__init__.py index 5973e5d1..b182cd84 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,3 +1,2 @@ - # Import future modules from __future__ import annotations diff --git a/test/manual_test/manual_test_application.py b/test/manual_test/manual_test_application.py index 71fedca2..dddf228d 100644 --- a/test/manual_test/manual_test_application.py +++ b/test/manual_test/manual_test_application.py @@ -1,6 +1,5 @@ """""" - # Import future modules from __future__ import annotations @@ -51,10 +50,7 @@ def test_get_background_color(self): def test_set_background_color(self, photoshop_app): self.app.backgroundColor.rgb.green = 0 - assert ( - self.app.backgroundColor.rgb.green - == photoshop_app.backgroundColor.rgb.green - ) + assert self.app.backgroundColor.rgb.green == photoshop_app.backgroundColor.rgb.green def test_build(self): assert self.app.build == "21.0 (20191018.r.37 2019/10/18: 614690fb487)" diff --git a/test/manual_test/manual_test_enum_values.py b/test/manual_test/manual_test_enum_values.py index d1182312..d81a8618 100644 --- a/test/manual_test/manual_test_enum_values.py +++ b/test/manual_test/manual_test_enum_values.py @@ -12,4 +12,4 @@ print(prefs.textFontSize) prefs.textFontSize = FontSize.Small print(prefs.textFontSize) -#app.eval_javascript("alert(app.preferences.textFontSize)") +# app.eval_javascript("alert(app.preferences.textFontSize)") diff --git a/test/manual_test/manual_test_path_items.py b/test/manual_test/manual_test_path_items.py index 1c2278ae..3adc1d2f 100644 --- a/test/manual_test/manual_test_path_items.py +++ b/test/manual_test/manual_test_path_items.py @@ -35,12 +35,8 @@ def setup(self): yield self.session.close() - def _create_path( - self, name: str, point_anchors: Sequence[tuple[float, float]] - ) -> PathItem: - point_infos: list[PathPointInfo] = [ - PathPointInfo(anchor=anchor) for anchor in point_anchors - ] + def _create_path(self, name: str, point_anchors: Sequence[tuple[float, float]]) -> PathItem: + point_infos: list[PathPointInfo] = [PathPointInfo(anchor=anchor) for anchor in point_anchors] last_anchor = point_anchors[-1] left = (last_anchor[0] + 1, last_anchor[1]) diff --git a/test/manual_test/manual_test_solid_color.py b/test/manual_test/manual_test_solid_color.py index 20229302..85e3f4d9 100644 --- a/test/manual_test/manual_test_solid_color.py +++ b/test/manual_test/manual_test_solid_color.py @@ -1,4 +1,5 @@ """""" + # Import third-party modules import pytest diff --git a/test/manual_test/manual_test_text_item.py b/test/manual_test/manual_test_text_item.py index 51c4867d..58abec1b 100644 --- a/test/manual_test/manual_test_text_item.py +++ b/test/manual_test/manual_test_text_item.py @@ -1,6 +1,5 @@ """""" - # Import future modules from __future__ import annotations @@ -21,9 +20,7 @@ class TestTextItem: @pytest.fixture(autouse=True) def setup(self, psd_file): """Setup for current test.""" - self.session = Session( - file_path=psd_file("textitem"), action="open", auto_close=True - ) + self.session = Session(file_path=psd_file("textitem"), action="open", auto_close=True) self.session.run_action() doc = self.session.active_document layer = doc.activeLayer