mullion 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mullion-0.1.0/.github/ISSUE_TEMPLATE/01-defect.yml +50 -0
- mullion-0.1.0/.github/ISSUE_TEMPLATE/02-change.yml +47 -0
- mullion-0.1.0/.github/ISSUE_TEMPLATE/03-depend.yml +41 -0
- mullion-0.1.0/.github/ISSUE_TEMPLATE/config.yml +23 -0
- mullion-0.1.0/.github/dependabot.yml +37 -0
- mullion-0.1.0/.github/pull_request_template.md +37 -0
- mullion-0.1.0/.github/workflows/ci.yml +121 -0
- mullion-0.1.0/.github/workflows/release.yml +148 -0
- mullion-0.1.0/.gitignore +24 -0
- mullion-0.1.0/CHANGELOG.md +45 -0
- mullion-0.1.0/CODEOWNERS +25 -0
- mullion-0.1.0/CODE_OF_CONDUCT.md +132 -0
- mullion-0.1.0/CONTRIBUTING.md +150 -0
- mullion-0.1.0/LICENSE +201 -0
- mullion-0.1.0/PKG-INFO +211 -0
- mullion-0.1.0/README.md +181 -0
- mullion-0.1.0/SECURITY.md +61 -0
- mullion-0.1.0/VERSIONING.md +58 -0
- mullion-0.1.0/pyproject.toml +82 -0
- mullion-0.1.0/src/mullion/__init__.py +111 -0
- mullion-0.1.0/src/mullion/background.py +304 -0
- mullion-0.1.0/src/mullion/page.py +146 -0
- mullion-0.1.0/src/mullion/py.typed +0 -0
- mullion-0.1.0/src/mullion/source.py +162 -0
- mullion-0.1.0/tests/conftest.py +140 -0
- mullion-0.1.0/tests/test_background.py +238 -0
- mullion-0.1.0/tests/test_page.py +132 -0
- mullion-0.1.0/tests/test_source.py +147 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Defect
|
|
2
|
+
description: An image comes out wrong, or a documented behaviour is not the behaviour.
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: >-
|
|
8
|
+
Every defect this library addresses is silent — a wrong answer renders
|
|
9
|
+
as a *plausible picture*. So the input is the report. Without it there
|
|
10
|
+
is nothing to reproduce, because every implementation in this space
|
|
11
|
+
produces a plausible picture for some input and the whole question is
|
|
12
|
+
which one.
|
|
13
|
+
- type: textarea
|
|
14
|
+
id: input
|
|
15
|
+
attributes:
|
|
16
|
+
label: The input
|
|
17
|
+
description: >-
|
|
18
|
+
Attach the file, or paste the code that builds it. A constructing
|
|
19
|
+
snippet is often better than a file — see tests/conftest.py, where
|
|
20
|
+
every fixture is built that way so a reader can see what makes it
|
|
21
|
+
awkward.
|
|
22
|
+
render: python
|
|
23
|
+
validations:
|
|
24
|
+
required: true
|
|
25
|
+
- type: textarea
|
|
26
|
+
id: call
|
|
27
|
+
attributes:
|
|
28
|
+
label: The call you made
|
|
29
|
+
description: The exact call, with its arguments.
|
|
30
|
+
render: python
|
|
31
|
+
validations:
|
|
32
|
+
required: true
|
|
33
|
+
- type: textarea
|
|
34
|
+
id: expected
|
|
35
|
+
attributes:
|
|
36
|
+
label: What you expected, and what you got
|
|
37
|
+
description: >-
|
|
38
|
+
Pixel values beat adjectives. `image.getpixel((0, 0))` returning
|
|
39
|
+
`(0, 0, 0)` where you wanted `(255, 255, 255)` is a report that can be
|
|
40
|
+
turned into a test in one step; "the background looks wrong" is not.
|
|
41
|
+
validations:
|
|
42
|
+
required: true
|
|
43
|
+
- type: input
|
|
44
|
+
id: versions
|
|
45
|
+
attributes:
|
|
46
|
+
label: Versions
|
|
47
|
+
description: Mullion, Pillow and Python.
|
|
48
|
+
placeholder: "mullion 0.1.0, Pillow 12.3.0, Python 3.13"
|
|
49
|
+
validations:
|
|
50
|
+
required: true
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Change
|
|
2
|
+
description: You want Mullion to do something it does not.
|
|
3
|
+
labels: ["enhancement"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: >-
|
|
8
|
+
Please read [what is out of
|
|
9
|
+
scope](https://github.com/sigrix-io/mullion#what-it-is-not) first.
|
|
10
|
+
The scope is narrow on purpose and a change that widens it will be
|
|
11
|
+
turned down however good it is — an application takes this library on
|
|
12
|
+
because it is small.
|
|
13
|
+
- type: textarea
|
|
14
|
+
id: problem
|
|
15
|
+
attributes:
|
|
16
|
+
label: What goes wrong today
|
|
17
|
+
description: >-
|
|
18
|
+
Describe the situation, not the feature. The most useful reports here
|
|
19
|
+
are "an image arrived in a shape you do not handle and was silently
|
|
20
|
+
accepted", which is the thesis of the whole library.
|
|
21
|
+
validations:
|
|
22
|
+
required: true
|
|
23
|
+
- type: textarea
|
|
24
|
+
id: workaround
|
|
25
|
+
attributes:
|
|
26
|
+
label: What you do instead right now
|
|
27
|
+
description: >-
|
|
28
|
+
If the answer is a few lines of Pillow in your own codebase, paste
|
|
29
|
+
them. That is the strongest possible case: it is what every caller
|
|
30
|
+
would otherwise be writing separately, which is exactly the argument
|
|
31
|
+
for it living here.
|
|
32
|
+
render: python
|
|
33
|
+
validations:
|
|
34
|
+
required: false
|
|
35
|
+
- type: dropdown
|
|
36
|
+
id: scope
|
|
37
|
+
attributes:
|
|
38
|
+
label: Does this handle one image, or several?
|
|
39
|
+
description: >-
|
|
40
|
+
The line is stated in the README and CONTRIBUTING: this library handles
|
|
41
|
+
one image, and composing several is the application's job.
|
|
42
|
+
options:
|
|
43
|
+
- One image
|
|
44
|
+
- Several images
|
|
45
|
+
- Not sure
|
|
46
|
+
validations:
|
|
47
|
+
required: true
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: I depend on this
|
|
2
|
+
description: Not a defect. Tell us you are building on 0.x so we do not break you silently.
|
|
3
|
+
labels: ["dependent"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: >-
|
|
8
|
+
[VERSIONING.md](https://github.com/sigrix-io/mullion/blob/main/VERSIONING.md)
|
|
9
|
+
says plainly that any `0.x` release may break any other. This form is
|
|
10
|
+
the other half of that bargain: the practical difference between an
|
|
11
|
+
announced breaking change and a surprising one is knowing somebody is
|
|
12
|
+
out there.
|
|
13
|
+
- type: input
|
|
14
|
+
id: version
|
|
15
|
+
attributes:
|
|
16
|
+
label: Which version are you pinned to?
|
|
17
|
+
placeholder: "mullion~=0.1.0"
|
|
18
|
+
validations:
|
|
19
|
+
required: true
|
|
20
|
+
- type: checkboxes
|
|
21
|
+
id: surface
|
|
22
|
+
attributes:
|
|
23
|
+
label: Which parts do you use?
|
|
24
|
+
description: >-
|
|
25
|
+
This is what tells us which change would actually cost you something.
|
|
26
|
+
options:
|
|
27
|
+
- label: "`normalize` / `open_bytes` / `open_path`"
|
|
28
|
+
- label: "`clean_background` and its defaults"
|
|
29
|
+
- label: "`page` geometry and DPI"
|
|
30
|
+
validations:
|
|
31
|
+
required: true
|
|
32
|
+
- type: textarea
|
|
33
|
+
id: context
|
|
34
|
+
attributes:
|
|
35
|
+
label: Anything else worth knowing
|
|
36
|
+
description: >-
|
|
37
|
+
What you are building, whether you have pinned the defaults, whether a
|
|
38
|
+
change in *output* would be a problem for you or merely a diff. No
|
|
39
|
+
obligation to name your project.
|
|
40
|
+
validations:
|
|
41
|
+
required: false
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# The three forms alongside this file are the three kinds CONTRIBUTING.md says
|
|
2
|
+
# are triaged differently. A blank issue would let the classification be
|
|
3
|
+
# skipped, which is the thing that does not currently work.
|
|
4
|
+
blank_issues_enabled: false
|
|
5
|
+
|
|
6
|
+
contact_links:
|
|
7
|
+
- name: Report a security vulnerability
|
|
8
|
+
url: https://github.com/sigrix-io/mullion/security/policy
|
|
9
|
+
about: >-
|
|
10
|
+
Do not open a public issue. Email security@sigrix.io — acknowledged
|
|
11
|
+
within 72 hours. This library decodes untrusted bytes; an input that
|
|
12
|
+
makes it misbehave is in scope.
|
|
13
|
+
- name: Raise a code of conduct concern
|
|
14
|
+
url: https://github.com/sigrix-io/mullion/blob/main/CODE_OF_CONDUCT.md
|
|
15
|
+
about: >-
|
|
16
|
+
Email conduct@sigrix.io. A different address and a different queue from
|
|
17
|
+
security reports; neither is a fallback for the other.
|
|
18
|
+
- name: A bug in Pillow itself
|
|
19
|
+
url: https://github.com/python-pillow/Pillow/issues
|
|
20
|
+
about: >-
|
|
21
|
+
Mullion is a thin layer over Pillow. If the wrong answer is Pillow's,
|
|
22
|
+
report it there — unless something here makes it worse or reachable, in
|
|
23
|
+
which case file a defect and say so.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Keeps the two things this repository pins from ageing quietly.
|
|
2
|
+
#
|
|
3
|
+
# The ruff version in ci.yml is pinned so that a local `ruff format` and the CI
|
|
4
|
+
# check can never disagree. That trade has a second half which is easy to leave
|
|
5
|
+
# unbuilt: a pin only stays correct if something bumps it, and left alone the
|
|
6
|
+
# formatter drifts away from whatever contributors have installed — the same
|
|
7
|
+
# failure pinning exists to prevent, arriving from the other direction and with
|
|
8
|
+
# a green check the whole way.
|
|
9
|
+
#
|
|
10
|
+
# The test matrix is what makes this safe rather than merely automatic. A bump
|
|
11
|
+
# that breaks anything arrives as a red check on a pull request nobody has
|
|
12
|
+
# merged, and because `ci-passed` is required on `main`, it cannot land without
|
|
13
|
+
# passing.
|
|
14
|
+
#
|
|
15
|
+
# Two GitHub behaviours worth knowing: this file is read from the default
|
|
16
|
+
# branch, so editing it on a branch changes nothing until it merges; and
|
|
17
|
+
# Dependabot needs the dependency graph enabled on the repository, which is not
|
|
18
|
+
# something this file can assert.
|
|
19
|
+
#
|
|
20
|
+
# Deliberately no auto-merge. Whether these should merge themselves when green
|
|
21
|
+
# is a judgement about how much unattended merging is wanted, and it belongs on
|
|
22
|
+
# the record rather than defaulted into by a config nobody revisits.
|
|
23
|
+
version: 2
|
|
24
|
+
updates:
|
|
25
|
+
# Both the runtime floor and the dev extras.
|
|
26
|
+
- package-ecosystem: "pip"
|
|
27
|
+
directory: "/"
|
|
28
|
+
schedule:
|
|
29
|
+
interval: "weekly"
|
|
30
|
+
|
|
31
|
+
# Turns an action deprecation into a pull request, rather than a warning at
|
|
32
|
+
# the top of a log nobody reads until the runtime is withdrawn and checkout
|
|
33
|
+
# starts failing on whichever pull request happens to be open that day.
|
|
34
|
+
- package-ecosystem: "github-actions"
|
|
35
|
+
directory: "/"
|
|
36
|
+
schedule:
|
|
37
|
+
interval: "weekly"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
## What this changes
|
|
2
|
+
|
|
3
|
+
<!-- One or two sentences. The diff shows what changed; say why. -->
|
|
4
|
+
|
|
5
|
+
## Does it change output?
|
|
6
|
+
|
|
7
|
+
<!--
|
|
8
|
+
The question that matters most in a library that produces pixels. Delete the
|
|
9
|
+
rows that do not apply.
|
|
10
|
+
|
|
11
|
+
- [ ] No — refactor, docs, tests or tooling only.
|
|
12
|
+
- [ ] Yes, and it is a fix — the previous output was wrong. Say for which
|
|
13
|
+
inputs, so the changelog entry can name them.
|
|
14
|
+
- [ ] Yes, and it is a default moving. CONTRIBUTING asks for the images:
|
|
15
|
+
what got better, and what got worse. There is no threshold that is right
|
|
16
|
+
for everything.
|
|
17
|
+
-->
|
|
18
|
+
|
|
19
|
+
## Have you watched the new tests fail?
|
|
20
|
+
|
|
21
|
+
<!--
|
|
22
|
+
CONTRIBUTING calls this the one house rule. Every defect this library addresses
|
|
23
|
+
is silent, so a test can assert the right thing about the right pixel and still
|
|
24
|
+
be worthless — break the implementation deliberately, confirm the test that
|
|
25
|
+
names the claim goes red, then put it back.
|
|
26
|
+
|
|
27
|
+
Say which mutation you tried and which test caught it.
|
|
28
|
+
-->
|
|
29
|
+
|
|
30
|
+
## Checks
|
|
31
|
+
|
|
32
|
+
- [ ] `python -m pytest`
|
|
33
|
+
- [ ] `python -m ruff check src/ tests/`
|
|
34
|
+
- [ ] `python -m ruff format --check src/ tests/`
|
|
35
|
+
- [ ] `python -m mypy --strict src/mullion`
|
|
36
|
+
- [ ] Scope: this still handles *one* image (see CONTRIBUTING)
|
|
37
|
+
- [ ] `CHANGELOG.md` updated under `Unreleased`, if this is user-visible
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Lint, then test, then prove the package builds and installs.
|
|
2
|
+
#
|
|
3
|
+
# The ruff steps run before pytest deliberately: a formatting slip should fail
|
|
4
|
+
# the job in ten seconds rather than after the suite. They are two commands
|
|
5
|
+
# rather than one because `check` and `format --check` answer different
|
|
6
|
+
# questions and only one of them is about style.
|
|
7
|
+
name: CI
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
branches: [main]
|
|
12
|
+
pull_request:
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
concurrency:
|
|
18
|
+
group: ci-${{ github.ref }}
|
|
19
|
+
cancel-in-progress: true
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
lint:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v5
|
|
26
|
+
- uses: actions/setup-python@v6
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.13"
|
|
29
|
+
# Pinned, so that a local `ruff format` and this check can never disagree
|
|
30
|
+
# about formatter behaviour. Dependabot bumps it; the tests gate the bump.
|
|
31
|
+
- run: python -m pip install ruff==0.16.5
|
|
32
|
+
- run: python -m ruff check src/ tests/
|
|
33
|
+
- run: python -m ruff format --check src/ tests/
|
|
34
|
+
|
|
35
|
+
test:
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
strategy:
|
|
38
|
+
fail-fast: false
|
|
39
|
+
matrix:
|
|
40
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v5
|
|
43
|
+
- uses: actions/setup-python@v6
|
|
44
|
+
with:
|
|
45
|
+
python-version: ${{ matrix.python-version }}
|
|
46
|
+
- run: python -m pip install -e ".[dev]"
|
|
47
|
+
- run: python -m pytest
|
|
48
|
+
|
|
49
|
+
# ``py.typed`` ships in the wheel and pyproject declares ``Typing :: Typed``,
|
|
50
|
+
# which together tell every consumer's type checker to trust these
|
|
51
|
+
# annotations instead of treating the package as untyped. Nothing verified
|
|
52
|
+
# them until #3, and a wrong annotation is the quietest defect a library can
|
|
53
|
+
# ship: it does not raise, it just makes somebody else's mypy run confidently
|
|
54
|
+
# green about the wrong thing. Pinned for the same reason ruff is — a
|
|
55
|
+
# floating checker turns an unrelated release into a red build on a commit
|
|
56
|
+
# that changed nothing.
|
|
57
|
+
types:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v5
|
|
61
|
+
- uses: actions/setup-python@v6
|
|
62
|
+
with:
|
|
63
|
+
python-version: "3.13"
|
|
64
|
+
- run: python -m pip install -e ".[dev]"
|
|
65
|
+
- run: python -m mypy --strict src/mullion
|
|
66
|
+
|
|
67
|
+
# A floor nobody tests is a guess. pyproject declares pillow>=10.0, so
|
|
68
|
+
# something has to actually run against 10.0 — otherwise the first user on an
|
|
69
|
+
# older Pillow discovers the real floor for us, in production.
|
|
70
|
+
oldest-pillow:
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
steps:
|
|
73
|
+
- uses: actions/checkout@v5
|
|
74
|
+
- uses: actions/setup-python@v6
|
|
75
|
+
with:
|
|
76
|
+
python-version: "3.11"
|
|
77
|
+
- run: python -m pip install -e ".[dev]"
|
|
78
|
+
- run: python -m pip install "pillow==10.0.*"
|
|
79
|
+
- run: python -m pytest
|
|
80
|
+
|
|
81
|
+
build:
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/checkout@v5
|
|
85
|
+
- uses: actions/setup-python@v6
|
|
86
|
+
with:
|
|
87
|
+
python-version: "3.13"
|
|
88
|
+
- run: python -m pip install build twine
|
|
89
|
+
- run: python -m build
|
|
90
|
+
# Catches a README that renders on GitHub and not on PyPI, which is
|
|
91
|
+
# otherwise found at the moment of publishing and not before.
|
|
92
|
+
- run: python -m twine check dist/*
|
|
93
|
+
# The wheel is what users get. Installing it from a clean directory is
|
|
94
|
+
# the only thing that proves py.typed and the package layout shipped —
|
|
95
|
+
# an editable install reads the source tree and would pass either way.
|
|
96
|
+
- name: The built wheel imports and carries its type marker
|
|
97
|
+
run: |
|
|
98
|
+
python -m venv /tmp/wheelcheck
|
|
99
|
+
/tmp/wheelcheck/bin/python -m pip install --quiet dist/*.whl
|
|
100
|
+
cd /tmp
|
|
101
|
+
/tmp/wheelcheck/bin/python -c "
|
|
102
|
+
import mullion, pathlib
|
|
103
|
+
print('version', mullion.__version__)
|
|
104
|
+
root = pathlib.Path(mullion.__file__).parent
|
|
105
|
+
assert (root / 'py.typed').exists(), 'py.typed did not ship'
|
|
106
|
+
for name in mullion.__all__:
|
|
107
|
+
assert hasattr(mullion, name), name
|
|
108
|
+
print(len(mullion.__all__), 'exports resolve')
|
|
109
|
+
"
|
|
110
|
+
|
|
111
|
+
# The aggregate gate. One check to require on main, so adding a job above
|
|
112
|
+
# does not mean editing a branch protection rule to match.
|
|
113
|
+
ci-passed:
|
|
114
|
+
if: always()
|
|
115
|
+
needs: [lint, types, test, oldest-pillow, build]
|
|
116
|
+
runs-on: ubuntu-latest
|
|
117
|
+
steps:
|
|
118
|
+
- name: Every job succeeded
|
|
119
|
+
run: |
|
|
120
|
+
echo '${{ toJSON(needs) }}'
|
|
121
|
+
[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "false" ]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Publish a tagged release to PyPI. Nothing else in this repository publishes,
|
|
2
|
+
# and nothing in this file runs on a branch — a release is a tag, deliberately,
|
|
3
|
+
# so that publishing is an act with a name rather than a side effect of a merge.
|
|
4
|
+
#
|
|
5
|
+
# Trusted publishing (OIDC) rather than an API token: the runner exchanges a
|
|
6
|
+
# short-lived identity for an upload, so there is no long-lived secret in this
|
|
7
|
+
# repository to store, leak or rotate. The trade is that PyPI has to be told
|
|
8
|
+
# which workflow in which environment it trusts, which is configuration that
|
|
9
|
+
# lives there and not here — see the note on `environment:` below.
|
|
10
|
+
name: Release
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
tags: ["v*"]
|
|
15
|
+
|
|
16
|
+
# Read-only by default. Only the publish job widens this, and only to the one
|
|
17
|
+
# permission the OIDC exchange needs.
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
|
|
21
|
+
# Deliberately NOT `cancel-in-progress`, which is what ci.yml sets. Cancelling
|
|
22
|
+
# a CI run wastes a runner; cancelling a run that is mid-upload can leave a
|
|
23
|
+
# release half-landed, and a PyPI version number can never be reused — not even
|
|
24
|
+
# after deleting the file. There is nothing to gain here by being fast.
|
|
25
|
+
concurrency:
|
|
26
|
+
group: release-${{ github.ref }}
|
|
27
|
+
cancel-in-progress: false
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
# Everything that can say no, said before anything leaves the runner.
|
|
31
|
+
build:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
# A fork can push a tag to itself; it must not be able to build the
|
|
34
|
+
# artifacts this repository's trust policy would then be asked to publish.
|
|
35
|
+
if: github.repository == 'sigrix-io/mullion'
|
|
36
|
+
steps:
|
|
37
|
+
# checkout and setup-python track ci.yml's pins on purpose, rather than
|
|
38
|
+
# the newest majors: this workflow runs once per release, at the moment
|
|
39
|
+
# when discovering an action's breaking change is most expensive, so it
|
|
40
|
+
# should run on the versions the rest of the repository has exercised.
|
|
41
|
+
# Dependabot moves both files together.
|
|
42
|
+
- uses: actions/checkout@v5
|
|
43
|
+
- uses: actions/setup-python@v6
|
|
44
|
+
with:
|
|
45
|
+
python-version: "3.13"
|
|
46
|
+
|
|
47
|
+
- run: python -m pip install build twine
|
|
48
|
+
- run: python -m build
|
|
49
|
+
|
|
50
|
+
# hatchling reads the version out of pyproject.toml; the tag is typed by
|
|
51
|
+
# hand. Nothing makes them agree. Tagging v0.2.0 without bumping
|
|
52
|
+
# pyproject builds 0.1.0 and would publish it under a tag saying
|
|
53
|
+
# otherwise — or, if 0.1.0 is already on PyPI, fail at the upload with a
|
|
54
|
+
# duplicate-file error naming neither cause. Ask here instead, where the
|
|
55
|
+
# answer is still cheap.
|
|
56
|
+
- name: The tag names the version that was actually built
|
|
57
|
+
run: |
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
60
|
+
# mullion-0.1.0-py3-none-any.whl -> 0.1.0. A wheel filename uses "-"
|
|
61
|
+
# as its field separator, so a PEP 440 version can never contain one.
|
|
62
|
+
built="$(basename "$(ls dist/*.whl)" | cut -d- -f2)"
|
|
63
|
+
echo "tag=${tag} built=${built}"
|
|
64
|
+
if [ "${tag}" != "${built}" ]; then
|
|
65
|
+
echo "::error::tag ${GITHUB_REF_NAME} does not name the packaged version ${built}. Bump pyproject.toml, or delete the tag and retag."
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# A release nobody wrote down is one a dependent cannot evaluate, which
|
|
70
|
+
# is the whole promise VERSIONING.md makes while this is 0.x. Keep a
|
|
71
|
+
# Changelog puts the version in a heading, so this is an exact check
|
|
72
|
+
# rather than a guess at prose.
|
|
73
|
+
- name: The changelog has an entry for this version
|
|
74
|
+
run: |
|
|
75
|
+
set -euo pipefail
|
|
76
|
+
version="${GITHUB_REF_NAME#v}"
|
|
77
|
+
if ! grep -qF "## [${version}]" CHANGELOG.md; then
|
|
78
|
+
echo "::error::CHANGELOG.md has no '## [${version}]' heading. Move the Unreleased entries under it before tagging."
|
|
79
|
+
exit 1
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Catches a README that renders on GitHub and not on PyPI, which is
|
|
83
|
+
# otherwise found at the moment of publishing and not before.
|
|
84
|
+
- run: python -m twine check dist/*
|
|
85
|
+
|
|
86
|
+
# ci.yml proves the *source tree* passes, via an editable install. This
|
|
87
|
+
# proves the *artifact* does, which is a different question and the only
|
|
88
|
+
# one that still matters once the bytes are immutable on PyPI: a file
|
|
89
|
+
# missing from the wheel is invisible to every editable-install run.
|
|
90
|
+
- name: The built wheel installs, imports, and passes the suite
|
|
91
|
+
run: |
|
|
92
|
+
set -euo pipefail
|
|
93
|
+
python -m venv /tmp/releasecheck
|
|
94
|
+
/tmp/releasecheck/bin/python -m pip install --quiet dist/*.whl pytest
|
|
95
|
+
# From /tmp, so nothing in the checkout can satisfy the import.
|
|
96
|
+
cd /tmp
|
|
97
|
+
/tmp/releasecheck/bin/python -c "
|
|
98
|
+
import mullion, pathlib
|
|
99
|
+
root = pathlib.Path(mullion.__file__).parent
|
|
100
|
+
assert (root / 'py.typed').exists(), 'py.typed did not ship'
|
|
101
|
+
for name in mullion.__all__:
|
|
102
|
+
assert hasattr(mullion, name), name
|
|
103
|
+
print(mullion.__version__, '-', len(mullion.__all__), 'exports resolve')
|
|
104
|
+
"
|
|
105
|
+
cd "${GITHUB_WORKSPACE}"
|
|
106
|
+
# src-layout means ./mullion does not exist, so the import below can
|
|
107
|
+
# only resolve to the wheel in the venv. Asserted rather than assumed:
|
|
108
|
+
# a suite that quietly re-tested the source tree would look identical.
|
|
109
|
+
/tmp/releasecheck/bin/python -c "
|
|
110
|
+
import mullion
|
|
111
|
+
assert 'site-packages' in mullion.__file__, mullion.__file__
|
|
112
|
+
"
|
|
113
|
+
/tmp/releasecheck/bin/python -m pytest tests/
|
|
114
|
+
|
|
115
|
+
- uses: actions/upload-artifact@v7
|
|
116
|
+
with:
|
|
117
|
+
name: dist
|
|
118
|
+
path: dist/
|
|
119
|
+
if-no-files-found: error
|
|
120
|
+
|
|
121
|
+
# Separate from the build so that a failure above cannot half-publish: this
|
|
122
|
+
# job does not start unless every check passed, and it builds nothing itself.
|
|
123
|
+
publish:
|
|
124
|
+
needs: build
|
|
125
|
+
runs-on: ubuntu-latest
|
|
126
|
+
environment:
|
|
127
|
+
# PyPI's trusted-publisher policy names this environment alongside the
|
|
128
|
+
# repository and the workflow filename, so the three have to match what
|
|
129
|
+
# is configured there. It is also where to add a required reviewer if a
|
|
130
|
+
# release should ever need a second pair of eyes.
|
|
131
|
+
name: pypi
|
|
132
|
+
url: https://pypi.org/p/mullion
|
|
133
|
+
permissions:
|
|
134
|
+
# The only elevated permission in this file, scoped to the one job that
|
|
135
|
+
# needs it. This mints the OIDC token PyPI exchanges for an upload, and
|
|
136
|
+
# it is also what makes the action attach PEP 740 attestations.
|
|
137
|
+
id-token: write
|
|
138
|
+
steps:
|
|
139
|
+
- uses: actions/download-artifact@v8
|
|
140
|
+
with:
|
|
141
|
+
name: dist
|
|
142
|
+
path: dist/
|
|
143
|
+
|
|
144
|
+
# Pinned to the commit rather than the tag. A tag is mutable by whoever
|
|
145
|
+
# owns the action, and this is the one step in this repository holding a
|
|
146
|
+
# credential worth stealing. Dependabot bumps the pin and rewrites the
|
|
147
|
+
# trailing comment with it.
|
|
148
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
mullion-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Python bytecode. CONTRIBUTING.md asks every contributor to run pytest and
|
|
2
|
+
# ruff before pushing, so these appear during a normal contribution rather than
|
|
3
|
+
# only on a maintainer's machine. Without this entry one routine `git add -A`
|
|
4
|
+
# commits build artefacts nobody wants to review.
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
|
|
8
|
+
# An editable install writes here, and `python -m build` writes the other two.
|
|
9
|
+
*.egg-info/
|
|
10
|
+
build/
|
|
11
|
+
dist/
|
|
12
|
+
|
|
13
|
+
# Installing without touching the system Python means a virtual environment.
|
|
14
|
+
# Both conventional names are listed so whichever a contributor reaches for
|
|
15
|
+
# stays untracked.
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
|
|
19
|
+
# Test and tooling caches.
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
htmlcov/
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are recorded here. The format is
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
follows [VERSIONING.md](VERSIONING.md) — read it before pinning, because
|
|
6
|
+
pre-1.0 means what it says.
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- The annotations `py.typed` promises are now verified. The wheel ships
|
|
13
|
+
`src/mullion/py.typed` and the package declares `Typing :: Typed`, which
|
|
14
|
+
together tell a consumer's type checker to trust these annotations — but
|
|
15
|
+
nothing had ever run one, and `mypy --strict` reported nine errors on the
|
|
16
|
+
first release. All nine were `Image.load()`, typed `PixelAccess | None` and
|
|
17
|
+
indexed without narrowing. Nothing raised: a wrong annotation just makes
|
|
18
|
+
somebody else's type checker confidently green about the wrong thing.
|
|
19
|
+
([#3](https://github.com/sigrix-io/mullion/issues/3))
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- `mullion.background`'s two internal pixel readers now assert the image mode
|
|
24
|
+
they were written for instead of assuming it, so a caller handing one the
|
|
25
|
+
wrong mode fails immediately rather than reading a shape nothing checks.
|
|
26
|
+
No public API changed.
|
|
27
|
+
|
|
28
|
+
## [0.1.0]
|
|
29
|
+
|
|
30
|
+
First release.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `mullion.source` — `normalize()`, and the `open_bytes()` / `open_path()`
|
|
35
|
+
adapters over it. Applies the EXIF orientation tag and resolves transparency
|
|
36
|
+
in all three shapes it arrives in: `RGBA`, `LA`, and palette `P` carrying a
|
|
37
|
+
`transparency` key in `info`. `keep_alpha` chooses between compositing onto a
|
|
38
|
+
matte and preserving the channel.
|
|
39
|
+
- `mullion.background` — `clean_background()`, border-seeded so that a
|
|
40
|
+
near-white region enclosed by the subject is never mistaken for the
|
|
41
|
+
background around it. `looks_like_white_background()` answers the same
|
|
42
|
+
verdict without doing the work, and `keeps_alpha()` names the one coupling
|
|
43
|
+
between the open call and the clean call.
|
|
44
|
+
- `mullion.page` — `resolve_page_geometry()` and `named_page_geometry()`,
|
|
45
|
+
returning pixels and the DPI they were derived at as one value.
|
mullion-0.1.0/CODEOWNERS
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Every change to this repository requires review from a maintainer.
|
|
2
|
+
#
|
|
3
|
+
# Merging is discretionary and stays with the maintainers — see
|
|
4
|
+
# CONTRIBUTING.md. Contributions are welcome; governance is not open.
|
|
5
|
+
#
|
|
6
|
+
# The owner is a team rather than a person so that reviewer load can be shared
|
|
7
|
+
# without editing this file for every change in personnel, and so the
|
|
8
|
+
# repository does not advertise a single point of failure to anyone deciding
|
|
9
|
+
# whether to depend on it. It may hold one person; the indirection is the point.
|
|
10
|
+
|
|
11
|
+
* @sigrix-io/mullion-maintainers
|
|
12
|
+
|
|
13
|
+
# The published surface. Everything under src/ is a compatibility commitment
|
|
14
|
+
# the moment someone pins to a release, so it is called out separately from the
|
|
15
|
+
# repository default even though the owner is currently the same.
|
|
16
|
+
#
|
|
17
|
+
# tests/ belongs here for a reason worth stating: the defects this library
|
|
18
|
+
# exists for are silent — a wrong answer renders as a plausible picture — so
|
|
19
|
+
# the tests are the only thing that can see them. A weakened test is a
|
|
20
|
+
# weakened guarantee, arriving as a green run.
|
|
21
|
+
|
|
22
|
+
/src/ @sigrix-io/mullion-maintainers
|
|
23
|
+
/tests/ @sigrix-io/mullion-maintainers
|
|
24
|
+
/pyproject.toml @sigrix-io/mullion-maintainers
|
|
25
|
+
/VERSIONING.md @sigrix-io/mullion-maintainers
|