Skip to content

Commit ce9fd98

Browse files
authored
Merge pull request #267 from kjd/housekeeping
Housekeeping
2 parents 35c57f2 + 809240c commit ce9fd98

12 files changed

Lines changed: 58 additions & 125 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ jobs:
5050
path: dist/
5151

5252
publish-to-pypi:
53-
name: >-
54-
Publish to PyPI
53+
name: Publish to PyPI
5554
if: startsWith(github.ref, 'refs/tags/v') # only publish to PyPI on version tag pushes
5655
needs:
5756
- build

.github/workflows/python-package.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2-
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3-
41
name: Python package
52

63
on:
@@ -16,6 +13,14 @@ jobs:
1613
runs-on: ubuntu-latest
1714
steps:
1815
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
16+
- name: Check the license copyright year is current
17+
run: |
18+
year="$(date -u +%Y)"
19+
license_year="$(grep -m1 '^Copyright' LICENSE.md | grep -oE '[0-9]{4}' | tail -1)"
20+
if [ "$license_year" != "$year" ]; then
21+
echo "::error file=LICENSE.md::Copyright year $license_year is not the current year $year; update LICENSE.md" >&2
22+
exit 1
23+
fi
1924
- name: Set up Python
2025
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
2126
with:
@@ -75,7 +80,7 @@ jobs:
7580
gil: '0'
7681
- python-version: '3.15t-dev'
7782
gil: '0'
78-
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
83+
runs-on: ubuntu-latest
7984
steps:
8085
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
8186
- name: Set up Python ${{ matrix.python-version }}

.gitignore

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
*.py[cod]
22
*.so
33
*.egg
4-
*.egg-info
5-
dist
6-
build
7-
eggs
8-
parts
9-
bin
10-
var
11-
sdist
12-
develop-eggs
13-
.installed.cfg
14-
lib
15-
lib64
16-
.idea
4+
*.egg-info/
5+
dist/
6+
build/
7+
sdist/
8+
.idea/
179
*.swp
1810

1911
.hypothesis/
12+
.mypy_cache/
13+
.pytest_cache/
14+
.ruff_cache/
2015
.coverage
2116
htmlcov/
17+
.DS_Store
18+
uv.lock

README.md

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ specification to be a local user-interface issue distinct from IDNA
4040
conversion functionality.
4141

4242
For example, "Königsgäßchen" is not a permissible label as capital letters
43-
are not allowed. UTS 46 will convert this into lower case prior to applying
43+
are not allowed. UTS #46 will convert this into lower case prior to applying
4444
the IDNA conversion.
4545

4646
```pycon
@@ -78,7 +78,7 @@ Exceptions carry machine-readable attributes so that applications
7878
do not need to parse the message: `code` is a short, stable identifier
7979
for the rule that failed (listed below); and, when the failure can be
8080
attributed to a particular character, `text` (the label, or domain for
81-
UTS 46 processing, being validated), `codepoint` (the offending
81+
UTS #46 processing, being validated), `codepoint` (the offending
8282
codepoint as an integer) and `position` (its 1-based index within
8383
`text`, as quoted in the message) are set. Each is `None` when it does
8484
not apply. Message wording is not part of the API and may change.
@@ -112,8 +112,8 @@ disallowed_codepoint 75 1 Königsgäßchen
112112
| `non_canonical_alabel` | An `xn--` label is not the canonical Punycode encoding of its U-label (a "fake A-label") |
113113
| `invalid_ascii` | Byte input is not ASCII |
114114
| `invalid_utf8` | Byte input is not UTF-8 |
115-
| `uts46_disallowed` | A codepoint is disallowed by the UTS 46 mapping table |
116-
| `uts46_std3` | An ASCII character is rejected by the UTS 46 STD3 rules |
115+
| `uts46_disallowed` | A codepoint is disallowed by the UTS #46 mapping table |
116+
| `uts46_std3` | An ASCII character is rejected by the UTS #46 STD3 rules |
117117
| `unsupported_errors` | The codec was given an `errors` handler other than `strict` |
118118

119119

@@ -146,32 +146,24 @@ the tool exits with a non-zero status if any conversion failed.
146146

147147
## Additional Notes
148148

149-
* **Version support**. This library supports Python 3.9 and higher.
149+
* **Python version support**. This library supports Python 3.9 and higher.
150150
As this library serves as a low-level toolkit for a variety of
151151
applications, we strive to support all versions of Python that are
152-
not beyond end-of-life.
152+
not beyond end-of-life. Free-threaded Python is also supported,
153+
as the library holds no mutable global state the functions can be
154+
called concurrently from multiple threads.
153155

154156
* **Unicode version**. The IDNA and UTS #46 lookup tables are generated
155-
from a specific Unicode release, available as `idna.unicode_version`
156-
(and shown by `idna --version`). Some checks — NFC normalisation,
157-
bidirectional classes and combining classes — still come from the
157+
from a specific Unicode release. Some Unicode data depends on the
158158
running Python's `unicodedata` module, so on an older Python a
159-
character new to Unicode may be rejected (typically with the
160-
`unknown_codepoint` or `bidi_unknown_direction` error codes) even
161-
though the tables know about it.
162-
163-
* **Thread safety and free-threaded Python**. The library holds no
164-
mutable global state — the lookup tables are read-only — so all
165-
functions may be called concurrently from multiple threads, and the
166-
incremental codec classes keep their state per instance. The test
167-
suite runs on free-threaded CPython builds (`3.14t`) with the GIL
168-
disabled and includes a concurrency test.
159+
character new to Unicode may be rejected as unknown even if this
160+
library knows about it.
169161

170162
* **Emoji**. It is an occasional request to support emoji domains in
171163
this library. Encoding of symbols like emoji is expressly prohibited by
172164
the IDNA technical standard, and emoji domains are broadly phased
173165
out across the domain industry due to associated security risks.
174166

175-
* **Regenerating lookup tables**. The IDNA and UTS 46 functionality
167+
* **Regenerating lookup tables**. The IDNA and UTS #46 functionality
176168
relies upon pre-calculated lookup tables, generated using the
177169
`idna-data` script in [`tools/`](tools/README.md).

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ Please disclose your issue through Github's
1515
[security advisory facility](https://github.com/kjd/idna/security/advisories/new).
1616

1717
We will endeavor to prioritize review, remediation and disclosure of
18-
vulnerabilites. However, be mindful that this project is maintained by a
18+
vulnerabilities. However, be mindful that this project is maintained by a
1919
team of volunteers who work on a best effort basis.

idna/codec.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ def _buffer_encode(self, data: str, errors: str, final: bool) -> tuple[bytes, in
7575
size += 1
7676
size += len(label)
7777

78-
# Join with U+002E
7978
result_bytes = b".".join(result) + trailing_dot
8079
size += len(trailing_dot)
8180
return result_bytes, size

idna/core.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -466,8 +466,9 @@ def check_label(label: str | bytes | bytearray) -> None:
466466
if len(label) == 0:
467467
raise IDNAError("Empty Label", code="empty_label")
468468

469-
# Reject on domain length rather than label length so support some UTS 46
470-
# use cases, still reducing processing of label contextual rules
469+
# Check against the domain length rather than the label length to
470+
# support some UTS #46 use cases, while still bounding the work done
471+
# by the label contextual rules below.
471472
if not valid_string_length(label, trailing_dot=True):
472473
raise IDNAError("Label too long", code="label_too_long")
473474

@@ -662,31 +663,27 @@ def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False
662663
if len(domain) > _max_input_length:
663664
raise IDNAError("Domain too long", code="input_too_long")
664665
if domain.isascii():
665-
# The only ASCII mapping in UTS #46 is upper- to lowercase; every
666-
# other ASCII codepoint has status V (tests pin this against the
667-
# table). ASCII is invariant under NFC, so this is the whole job.
666+
# The only ASCII mapping in UTS #46 is upper- to lowercase, and
667+
# ASCII is invariant under NFC, so lowercasing is the whole job.
668668
result = domain.lower()
669669
if std3_rules:
670670
_check_std3(result, domain, 0)
671671
return result
672672

673673
from .uts46data import uts46_replacements, uts46_starts, uts46_statuses
674674

675-
# Characters that pass through unchanged are not copied one at a time:
676-
# ``start`` marks the beginning of the current run of unchanged input,
677-
# and a run is only sliced out when a character has to be replaced or
678-
# dropped. For the common case where nothing changes no copy is made.
679-
# The STD3 check is applied to each piece of output as it is produced,
680-
# so that a violation is reported at its position in the input.
675+
# ``start`` marks the run of unchanged input not yet copied; a run is
676+
# only sliced out when a character must be replaced or dropped, so the
677+
# common no-change case makes no copy. STD3 is checked per output piece
678+
# to report a violation at its input position.
681679
output: list[str] = []
682680
start = 0
683681
for pos, char in enumerate(domain):
684682
code_point = ord(char)
685683
i = code_point if code_point < 256 else bisect.bisect_right(uts46_starts, code_point) - 1
686684
status = uts46_statuses[i]
687-
# UTS #46 §4: V is always valid, D is deviation (kept: transitional
688-
# processing, which mapped it, is deprecated), M is mapped, I is
689-
# ignored, anything else is disallowed.
685+
# UTS #46 §4: V valid, D deviation (kept), M mapped, I ignored,
686+
# anything else disallowed.
690687
if status == _STATUS_VALID:
691688
continue
692689
if status == _STATUS_MAPPED:
@@ -773,8 +770,6 @@ def encode(
773770
if uts46:
774771
s = uts46_remap(s, std3_rules)
775772

776-
# Reject inputs that exceed the maximum DNS domain length up-front
777-
# to avoid expensive computation on long inputs.
778773
if not valid_string_length(s, trailing_dot=True):
779774
raise IDNAError("Domain too long", code="domain_too_long")
780775

@@ -840,8 +835,6 @@ def decode(
840835
raise IDNAError("Domain too long", code="input_too_long")
841836
if uts46:
842837
s = uts46_remap(s, std3_rules, False)
843-
# Reject inputs that exceed the maximum DNS domain length up-front
844-
# to avoid expensive computation on long inputs.
845838
if not valid_string_length(s, trailing_dot=True):
846839
raise IDNAError("Domain too long", code="domain_too_long")
847840
trailing_dot = False

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ all = [
5353
"ty >= 0.0.37",
5454
"pytest >= 8.3.2",
5555
"hypothesis >= 6.141.1",
56+
"coverage >= 7.10.0",
5657
]
5758

5859
[tool.coverage.run]

tests/test_idna.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ def test_check_bidi(self):
138138

139139
# RFC 5893 Rule 2
140140
self.assertTrue(idna.check_bidi(r + al))
141-
self.assertTrue(idna.check_bidi(r + al))
142141
self.assertTrue(idna.check_bidi(r + an))
143142
self.assertTrue(idna.check_bidi(r + en))
144143
self.assertTrue(idna.check_bidi(r + es + al))

tests/test_idna_codec.py

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66

77
CODEC_NAME = "idna2008"
88

9+
# (decoded, encoded) pairs derived from CPython's Lib/test/test_codecs.py
10+
INCREMENTAL_TESTS = (
11+
("python.org", b"python.org"),
12+
("python.org.", b"python.org."),
13+
("pyth\xf6n.org", b"xn--pythn-mua.org"),
14+
("pyth\xf6n.org.", b"xn--pythn-mua.org."),
15+
)
16+
917

1018
class IDNACodecTests(unittest.TestCase):
1119
def setUp(self):
@@ -58,16 +66,7 @@ def encode(obj):
5866
return self.idnatests.test_encode(encode=encode)
5967

6068
def testIncrementalDecoder(self):
61-
# Tests derived from Python standard library test/test_codecs.py
62-
63-
incremental_tests = (
64-
("python.org", b"python.org"),
65-
("python.org.", b"python.org."),
66-
("pyth\xf6n.org", b"xn--pythn-mua.org"),
67-
("pyth\xf6n.org.", b"xn--pythn-mua.org."),
68-
)
69-
70-
for decoded, encoded in incremental_tests:
69+
for decoded, encoded in INCREMENTAL_TESTS:
7170
self.assertEqual(
7271
"".join(codecs.iterdecode((bytes([c]) for c in encoded), CODEC_NAME)),
7372
decoded,
@@ -106,15 +105,7 @@ def testIncrementalDecoder(self):
106105
self.assertEqual(decoder.decode(b"", True), "")
107106

108107
def testIncrementalEncoder(self):
109-
# Tests derived from Python standard library test/test_codecs.py
110-
111-
incremental_tests = (
112-
("python.org", b"python.org"),
113-
("python.org.", b"python.org."),
114-
("pyth\xf6n.org", b"xn--pythn-mua.org"),
115-
("pyth\xf6n.org.", b"xn--pythn-mua.org."),
116-
)
117-
for decoded, encoded in incremental_tests:
108+
for decoded, encoded in INCREMENTAL_TESTS:
118109
self.assertEqual(b"".join(codecs.iterencode(decoded, CODEC_NAME)), encoded)
119110

120111
encoder = codecs.getincrementalencoder(CODEC_NAME)()

0 commit comments

Comments
 (0)