Repository navigation
Conversation
…#3) Modern resolved only ASCII "'" in the pre-normalization quote pass, so a typographic apostrophe survived it, was transliterated to "'" and then deleted by the post-transliteration pass: "don't" slugged "don-t" while "don’t" slugged "dont". U+2018, U+2019 and U+02BC now join ASCII "'" in that first pass, matching what modern already produced under allow_unicode. Quotes a backend generates stay ASCII-only and are still removed, preserving the upstream #17/#23 distinction, so "Іванько" still slugs "ivanko". Legacy output is untouched and the frozen 2,688-case differential still passes. The one modern golden that encoded the old behavior ("Baby’s shoes" -> "babys-shoes") is updated, alongside a new legacy assertion pinning the frozen divergence. 🚀 Generated with [Dojo](https://heydojo.ai) ⛩️
Revisión adversarial independiente — APROBABLE (con nota de alcance)Revisado por quien no escribió el código, worktree propio ( Vista roja/verde (efecto, no forma). Revertí Efecto verificado (modern unifica la familia; legacy congelado): El diferencial legacy congelado de 2.688 casos ( Puerta fiel al CI ( Nota de alcance (no bloqueante). El diff toca Sin reparos sobre el código. — revisión adversarial automática, sesión Claude Code (independiente del autor). |
Issue
Referencia (sin solicitud de cierre automático): #3
«Modern: typographic apostrophe (U+2018/U+2019) slugs differently from ASCII '»
Causa raíz
slugify/slugify.pyresolvía sólo el apóstrofe ASCII en el paso de comillas previo a lanormalización (
QUOTE_PATTERN = re.compile(r"[']+")→ separador). Un apóstrofe tipográfico nocoincidía ahí, sobrevivía al paso, el backend lo transliteraba a
'ASCII y entonces lo borraba elsegundo paso (
QUOTE_PATTERN.sub('', text)). De ahí la divergencia:don't(U+0027)don-tdon-tdon-tdon’t(U+2019)dontdon-tdontdon‘t(U+2018)dontdon-tdontdonʼt(U+02BC)dontdon-tdontEl arreglo une U+2018, U+2019 y U+02BC al
'ASCII en ese primer paso, que es justo lo quemodern ya producía con
allow_unicode=True(don-t), así que el cambio alinea los dos modos demodern entre sí.
Cambio mínimo
slugify/slugify.py: nueva constanteMODERN_APOSTROPHE_PATTERN = re.compile(r"['\u02bc\u2018\u2019]+"),usada sólo en el paso previo a la normalización. El segundo paso (
QUOTE_PATTERN.sub('', text))se deja ASCII a propósito: las comillas fonéticas que genera el backend se siguen borrando, no
se convierten en separador (distinción upstream docs: nota de prueba 3 en README.md (1791997097) #17/docs: ejemplo de smart_truncate con save_order en el README #23;
Іванькоsigue dandoivanko).Clausa añadida al docstring de
slugify()que enumera los deltas de modern.tests/test_modern_unicode.py(nuevo): 8 tests de regresión.tests/test_release.py: golden actualizado (ver «Desviaciones de alcance»).Legacy intacto:
slugify/_legacy.pyno se toca y el diferencial congelado de 2.688 casos siguepasando (
tests/test_release.py::test_legacy_goldens_and_differential).Propagación: qué otros caminos comparten el mismo caso
No usé ninguna lección de memoria durable (la búsqueda en el cerebro compartido devolvió 0 resultados
para
python-slugify modern algorithm apostrophe QUOTE_PATTERN unicode regex lesson, en modo léxicodegradado; los dos catálogos
MEMORY.mdlocales están vacíos). Aun así relevé el patrón«comillas sólo-ASCII en el paso de entrada» en todo el repo:
slugify/slugify.py:18QUOTE_PATTERNslugify/_legacy.py:22,137,146dont(fijado por test)tools/legacy_reference.py:20,119,161slugify/special.pyslugify/__main__.pyslugify(), así que el CLI hereda el arregloCLI verificado:
python -m slugify --algorithm modern 'don’t'→don-t;python -m slugify 'don’t'(legacy por defecto) →
dont.Evidencia
Vista roja (el test distingue el efecto)
Sólo se revierte el código de producción; los tests se quedan. Bytecode limpiado entre corrida y
corrida (
rm -rf slugify/__pycache__ tests/__pycache__+-p no:cacheprovider).Vista verde (suite COMPLETA, no sólo el test nuevo)
Base antes del cambio, mismo entorno:
133 passed, 5 skipped, 136 subtests passed(+8 tests nuevos;los 2 skips extra son los backends
unidecode/anyasciiausentes en este entorno).Puerta de calidad reproducida y sobre qué versiones
Workflows leídos:
.github/workflows/taller-ci.yml(check requerido en PRs del worker; matriz3.11, 3.12,
fail-fast: false),.github/workflows/main.yml(pull_request, un job Linux quecorre
toxcompleto sobre pypy3.11 y 3.10–3.14),.github/workflows/ci.ymlydev.yml(sólo push a
ci/stagingysandbox/dev; no aplican a este PR).Reproducido aquí sólo con lo ya instalado y sin salir del worktree (venv local
env/,gitignoreado, con
pip install -e . pytest flake8 pycodestyle, es decir exactamente el install detaller-ci):
python -m pytest tests→141 passed, 7 skippedflake8 --ignore=E501,F403,F401,E241,E225,E128 --exclude=legacy_reference.py slugify setup.py tests tools→ exit 0pycodestyle --ignore=E128,E261,E225,E501,W605 --exclude=legacy_reference.py slugify tests setup.py tools→ exit 0tox.inireproducidos sobre 3.11.2:mypy(strict,packages = slugify) →Success: no issues found in 6 source files.No reproducidas (exigirían descargar intérpretes o escribir fuera del worktree, prohibido aquí):
Python 3.12 de taller-ci y pypy3.11, 3.10, 3.12, 3.13, 3.14 de main.yml, así como los entornos
tox por backend (
unidecodesin text-unidecode,anyascii) ytox -e packaging(
tools/check_dist.pyconstruye en directorios temporales externos). Sintaxis de riesgo revisada alleer los workflows y el diff: el cambio no introduce sintaxis nueva — una constante
re.compilecon escapes
\uXXXXdentro de string crudo (soportados porredesde Python 3.3) y f-strings que elrepo ya usa; nada de
match, walrus, genéricos PEP 695 ni nada posterior a 3.10. Riesgo de parseo enversiones viejas: ninguno identificado.
Alcance del diff
git diff --cached --statdel commit30721fc:Sin lockfiles, sin ficheros ajenos a la Issue, sin secretos ni configuración local
(
.env*,.mcp.json,.claude/settings*.jsonsiguen gitignoreados y fuera del commit). Losartefactos de trabajo (venv
env/, caches) están gitignoreados y no se commitearon.Desviación respecto a los
Archivos:de la Issue (que listabaslugify/slugify.pyytests/test_modern_unicode.py): se toca ademástests/test_release.py, y es forzoso —test_normalization_ordinals_dashes_and_phonetic_quotesfijaba como golden de modern justo elcomportamiento que la Issue declara bug (
slugify('Baby’s shoes') == 'babys-shoes'). Sin actualizarlola suite queda roja. Se cambió a
'baby-s-shoes'y se añadió una aserción legacy(
public_slugify('Baby’s shoes') == 'babys-shoes') que fija la divergencia congelada. Esto sigue laconvención del repo:
cf6cd3d(un33k#201) también actualizótests/test_release.pydentro de un PR defeature.
Fuera de alcance / seguimiento
docs/release-9/migration.md(~líneas 71-72) dice:«ASCII and curly apostrophes remain distinct; use explicit pre replacements if your application
needs a consistent apostrophe boundary». Tras este cambio esa frase es falsa para modern
(sigue siendo cierta para legacy). No la edité porque la convención del repo, verificada en
git log, es que los PRs de feature tocan sólo código+tests y los ajustes de docs/CHANGELOG van encommits
docs:/Releaseaparte (p. ej.c9e9698no tocó docs;54d81f6fue un commitdocs:independiente). Digo dónde está para que el maintainer decida: puedo incluirla en este PR si se
prefiere. Propongo algo como: «Legacy keeps ASCII and curly apostrophes distinct; modern maps the
U+2018/U+2019/U+02BC apostrophe family to a separator like ASCII
'. Backend-generated phoneticquotes are still removed in both modes.»
CHANGELOG.mdsin tocar: no hay sección «Unreleased» y el número de versión es decisión de release(AGENTS.md).
docs/release-9/historical-review.md(tabla T3, filaBaby’s shoes→babys-shoes) se deja talcual: es un registro de mediciones fechado, y AGENTS.md pide preservar las notas históricas como
evidencia datada.
U+02BC; no amplié el conjunto. Estos siguen igual que hoy y siguen coincidiendo con legacy
(
dont), fijado portest_quote_like_characters_outside_the_family_are_unchanged:U+201B
‛, U+2032′, U+FF07', U+02B9ʹ, U+05F3׳.Gap concreto que reporto honestamente: U+201B, U+2032 y U+05F3 sí dan
don-tconallow_unicode=True, o sea que siguen siendo internamente inconsistentes entre los dos modos demodern (U+FF07 da
donten ambos y U+02B9 conserva el carácter en modo unicode). Si se quiere lafamilia completa, eso es una Issue nueva, no este arreglo.
🚀 Generated with Dojo ⛩️