xml-cmpr 0.2.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.
- xml_cmpr-0.2.0/.gitignore +15 -0
- xml_cmpr-0.2.0/LICENSE +21 -0
- xml_cmpr-0.2.0/PKG-INFO +389 -0
- xml_cmpr-0.2.0/README.md +363 -0
- xml_cmpr-0.2.0/docs/README.md +8 -0
- xml_cmpr-0.2.0/docs/analyses/README.md +18 -0
- xml_cmpr-0.2.0/pyproject.toml +66 -0
- xml_cmpr-0.2.0/src/xml_cmpr/__init__.py +76 -0
- xml_cmpr-0.2.0/src/xml_cmpr/archive.py +176 -0
- xml_cmpr-0.2.0/src/xml_cmpr/campaign.py +77 -0
- xml_cmpr-0.2.0/src/xml_cmpr/canonical.py +111 -0
- xml_cmpr-0.2.0/src/xml_cmpr/cli.py +261 -0
- xml_cmpr-0.2.0/src/xml_cmpr/diff.py +181 -0
- xml_cmpr-0.2.0/src/xml_cmpr/discovery.py +40 -0
- xml_cmpr-0.2.0/src/xml_cmpr/model.py +207 -0
- xml_cmpr-0.2.0/src/xml_cmpr/py.typed +0 -0
- xml_cmpr-0.2.0/src/xml_cmpr/report.py +184 -0
- xml_cmpr-0.2.0/src/xml_cmpr/resources/__init__.py +0 -0
- xml_cmpr-0.2.0/src/xml_cmpr/resources/normalize.xsl +94 -0
- xml_cmpr-0.2.0/tests/conftest.py +28 -0
- xml_cmpr-0.2.0/tests/fixtures/a/absent_de_b.xml +2 -0
- xml_cmpr-0.2.0/tests/fixtures/a/attributs.xml +4 -0
- xml_cmpr-0.2.0/tests/fixtures/a/factures/facture_001.xml +9 -0
- xml_cmpr-0.2.0/tests/fixtures/a/identique.xml +9 -0
- xml_cmpr-0.2.0/tests/fixtures/a/malforme.xml +2 -0
- xml_cmpr-0.2.0/tests/fixtures/a/mixte.xml +2 -0
- xml_cmpr-0.2.0/tests/fixtures/a/notes.txt +1 -0
- xml_cmpr-0.2.0/tests/fixtures/a/texte.xml +2 -0
- xml_cmpr-0.2.0/tests/fixtures/b/attributs.xml +4 -0
- xml_cmpr-0.2.0/tests/fixtures/b/factures/facture_001.xml +10 -0
- xml_cmpr-0.2.0/tests/fixtures/b/identique.xml +4 -0
- xml_cmpr-0.2.0/tests/fixtures/b/malforme.xml +2 -0
- xml_cmpr-0.2.0/tests/fixtures/b/mixte.xml +2 -0
- xml_cmpr-0.2.0/tests/fixtures/b/notes.txt +1 -0
- xml_cmpr-0.2.0/tests/fixtures/b/seulement_en_b.xml +2 -0
- xml_cmpr-0.2.0/tests/fixtures/b/texte.xml +2 -0
- xml_cmpr-0.2.0/tests/test_archive.py +253 -0
- xml_cmpr-0.2.0/tests/test_campaign.py +24 -0
- xml_cmpr-0.2.0/tests/test_canonical.py +132 -0
- xml_cmpr-0.2.0/tests/test_cli.py +234 -0
- xml_cmpr-0.2.0/tests/test_diff.py +178 -0
- xml_cmpr-0.2.0/tests/test_discovery.py +34 -0
- xml_cmpr-0.2.0/tests/test_docs.py +97 -0
- xml_cmpr-0.2.0/tests/test_report.py +223 -0
- xml_cmpr-0.2.0/uv.lock +134 -0
xml_cmpr-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Frédéric Laurent
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
xml_cmpr-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: xml-cmpr
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Compare des documents XML issus de deux sources et produit un rapport des différences
|
|
5
|
+
Project-URL: Homepage, https://framagit.org/opikanoba/xml-compare
|
|
6
|
+
Project-URL: Repository, https://framagit.org/opikanoba/xml-compare.git
|
|
7
|
+
Project-URL: Issues, https://framagit.org/opikanoba/xml-compare/-/issues
|
|
8
|
+
Author-email: Frédéric Laurent <flt@opikanoba.org>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: canonicalisation,comparaison,diff,saxon,xml,xslt
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Natural Language :: French
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Classifier: Topic :: Text Processing :: Markup :: XML
|
|
23
|
+
Requires-Python: >=3.13
|
|
24
|
+
Requires-Dist: saxonche>=13.0.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# xml-cmpr
|
|
28
|
+
|
|
29
|
+
Compare des documents XML produits par **deux chaînes de production différentes** et
|
|
30
|
+
produit un rapport des différences, fichier par fichier.
|
|
31
|
+
|
|
32
|
+
L'outil parcourt la source A, cherche dans la source B le fichier de même chemin
|
|
33
|
+
relatif (les fichiers sans homologue sont ignorés), compare les deux documents sur leur
|
|
34
|
+
**forme canonique**, et écrit un rapport Markdown et/ou JSON dans le répertoire de
|
|
35
|
+
sortie.
|
|
36
|
+
|
|
37
|
+
Chaque source est soit un répertoire, soit une **archive ZIP** — les deux peuvent se
|
|
38
|
+
mélanger.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Le paquet est publié sur PyPI sous le nom **`xml-cmpr`**, qui est aussi celui de la
|
|
43
|
+
commande installée :
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
uv tool install xml-cmpr # ou : pipx install xml-cmpr
|
|
47
|
+
xml-cmpr --help
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Python 3.13 ou 3.14 est requis (le socle de `saxonche`, qui apporte le moteur XSLT).
|
|
51
|
+
|
|
52
|
+
Le paquet lui-même est en Python pur, mais `saxonche` n'est publié qu'en roues natives,
|
|
53
|
+
sans source à compiler. L'installation n'aboutit donc que sur les plateformes pour
|
|
54
|
+
lesquelles il existe une roue :
|
|
55
|
+
|
|
56
|
+
| Système | Architectures |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Linux (glibc 2.24 ou plus, « manylinux ») | x86_64, aarch64 |
|
|
59
|
+
| macOS (11 ou plus) | x86_64, arm64 |
|
|
60
|
+
| Windows | x86_64 |
|
|
61
|
+
|
|
62
|
+
Ailleurs — Alpine et autres Linux musl, processeurs 32 bits, PyPy, Python 3.15 —,
|
|
63
|
+
l'installation échoue en résolvant `saxonche` (pip : « No matching distribution found
|
|
64
|
+
for saxonche » ; uv : aucune roue pour la plateforme courante) : ce message signale une
|
|
65
|
+
plateforme non prise en charge, pas un défaut du paquet.
|
|
66
|
+
Pour travailler sur le dépôt lui-même :
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
uv sync
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Utilisation
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
uv run xml-cmpr SOURCE_A SOURCE_B REPERTOIRE_SORTIE
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
où `SOURCE_A` et `SOURCE_B` sont chacun un répertoire ou une archive ZIP :
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
uv run xml-cmpr chaine_a/ chaine_b/ rapport/
|
|
82
|
+
uv run xml-cmpr chaine_a.zip chaine_b.zip rapport/
|
|
83
|
+
uv run xml-cmpr chaine_a.zip chaine_b/ rapport/
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Produit `REPERTOIRE_SORTIE/rapport.md` et `REPERTOIRE_SORTIE/rapport.json`
|
|
87
|
+
(`--format` choisit les formats écrits) :
|
|
88
|
+
|
|
89
|
+
```markdown
|
|
90
|
+
# Rapport de comparaison XML
|
|
91
|
+
|
|
92
|
+
- **Source A** : `tests/fixtures/a`
|
|
93
|
+
- **Source B** : `tests/fixtures/b`
|
|
94
|
+
- **Date** : 2026-08-18 11:45
|
|
95
|
+
- Fichiers analysés : **5** · identiques : **1** · différents : **3** · en erreur : **1**
|
|
96
|
+
- Ignorés (absents de la source B) : **1**
|
|
97
|
+
|
|
98
|
+
## Fichiers différents
|
|
99
|
+
|
|
100
|
+
### `factures/facture_001.xml` — 1 différence
|
|
101
|
+
|
|
102
|
+
| Chemin | Type | A | B |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| `/Facture/Lignes/Ligne[2]` | élément ajouté en B | — | `<Ligne id="2">deux` |
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Options
|
|
108
|
+
|
|
109
|
+
| Option | Effet |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `--pattern GLOB` | motif des fichiers de A (défaut : `*.xml`) |
|
|
112
|
+
| `--no-recursive` | ne pas descendre dans les sous-répertoires |
|
|
113
|
+
| `--keep-whitespace` | ne pas normaliser les espaces des nœuds texte |
|
|
114
|
+
| `--exclude FICHIER` | fichier de XPath à exclure de la comparaison |
|
|
115
|
+
| `--max-diffs-per-file N` | différences détaillées par fichier (défaut : 50, `0` = illimité) |
|
|
116
|
+
| `--no-size-limit` | extraire une archive ZIP quels que soient sa taille et son nombre d'entrées (défaut : refus au-delà de 1 Gio ou de 100 000 entrées) |
|
|
117
|
+
| `--format FORMATS` | formats du rapport : `md`, `json` (défaut : `md,json`) |
|
|
118
|
+
| `--fail-on-diff` | code de sortie 1 si au moins une différence |
|
|
119
|
+
| `--fail-on-error` | code de sortie 3 si au moins un fichier est illisible |
|
|
120
|
+
| `-v`, `--verbose` | progression sur stderr |
|
|
121
|
+
|
|
122
|
+
Codes de sortie : `0` succès · `1` différences détectées (avec `--fail-on-diff`) ·
|
|
123
|
+
`2` erreur d'usage (source introuvable ou illisible, sortie inutilisable, XPath
|
|
124
|
+
d'exclusion invalide, format inconnu) · `3` fichiers en erreur (avec
|
|
125
|
+
`--fail-on-error`).
|
|
126
|
+
|
|
127
|
+
Un fichier XML malformé n'interrompt pas la campagne : il est reporté comme fichier en
|
|
128
|
+
erreur, et **sans `--fail-on-error` la campagne se termine quand même en `0`**. En
|
|
129
|
+
intégration continue, poser les deux drapeaux : `--fail-on-diff` seul laisse passer un
|
|
130
|
+
corpus entièrement illisible. Quand les deux conditions sont réunies, `3` l'emporte —
|
|
131
|
+
une campagne incomplète est un résultat moins sûr qu'une campagne qui a tout comparé.
|
|
132
|
+
|
|
133
|
+
### Le rapport JSON
|
|
134
|
+
|
|
135
|
+
`rapport.json` contient le même bilan que le Markdown, sous une forme directement
|
|
136
|
+
consommable par un autre traitement :
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"summary": {
|
|
141
|
+
"source_a": "chaine_a",
|
|
142
|
+
"source_b": "chaine_b",
|
|
143
|
+
"generated_at": "2026-08-18T11:45:12+02:00",
|
|
144
|
+
"analyzed": 5,
|
|
145
|
+
"identical": 1,
|
|
146
|
+
"different": 3,
|
|
147
|
+
"errors": 1,
|
|
148
|
+
"skipped": 1
|
|
149
|
+
},
|
|
150
|
+
"files": [
|
|
151
|
+
{
|
|
152
|
+
"path": "factures/facture_001.xml",
|
|
153
|
+
"status": "different",
|
|
154
|
+
"differences": [
|
|
155
|
+
{
|
|
156
|
+
"path": "/Facture/Lignes/Ligne[2]",
|
|
157
|
+
"kind": "element_added",
|
|
158
|
+
"label": "élément ajouté en B",
|
|
159
|
+
"a": null,
|
|
160
|
+
"b": "<Ligne id=\"2\">deux"
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
{ "path": "identique.xml", "status": "identique" },
|
|
165
|
+
{ "path": "malforme.xml", "status": "erreur", "error": "SXXP0003 …" }
|
|
166
|
+
],
|
|
167
|
+
"skipped": ["absent_de_b.xml"]
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- `generated_at` date la campagne, en ISO 8601 à la seconde, heure locale avec son
|
|
172
|
+
décalage ; c'est la même date que celle du rapport Markdown.
|
|
173
|
+
- `files` liste **tous** les fichiers analysés, un par chemin relatif ;
|
|
174
|
+
`status` vaut `identique`, `different` ou `erreur`.
|
|
175
|
+
- `differences` n'apparaît que pour un fichier `different` ; `error` que pour un fichier
|
|
176
|
+
`erreur` ; `truncated` (nombre de différences non détaillées) que si
|
|
177
|
+
`--max-diffs-per-file` a coupé la liste.
|
|
178
|
+
- `kind` est la nature de la différence, stable et utilisable en code —
|
|
179
|
+
`element_added`, `element_removed`, `element_renamed`, `attribute_added`,
|
|
180
|
+
`attribute_removed`, `attribute_changed`, `text_changed`, `text_added`, `text_removed`
|
|
181
|
+
— et `label` en est le libellé français affiché au rapport Markdown.
|
|
182
|
+
- `a` et `b` sont les valeurs de part et d'autre, `null` quand la valeur n'existe pas
|
|
183
|
+
du côté correspondant. Pour `element_renamed`, ce sont les deux noms : le nom local
|
|
184
|
+
quand l'élément n'a pas d'espace de noms (`Date`), `{uri}local` sinon
|
|
185
|
+
(`{urn:facture}Date`).
|
|
186
|
+
- `skipped` liste les fichiers de A sans homologue dans B.
|
|
187
|
+
|
|
188
|
+
### Sources en archive ZIP
|
|
189
|
+
|
|
190
|
+
Une source ZIP est extraite dans un répertoire temporaire, supprimé à la fin de la
|
|
191
|
+
campagne ; la comparaison, les options et le rapport sont ensuite exactement les mêmes
|
|
192
|
+
que pour un répertoire (le rapport nomme l'archive, pas le temporaire).
|
|
193
|
+
|
|
194
|
+
Si l'archive enveloppe tout son contenu dans un unique répertoire de tête, celui-ci est
|
|
195
|
+
traversé : deux archives dont les répertoires de tête s'appellent `chaine_a/` et
|
|
196
|
+
`chaine_b/` s'apparient quand même sur le chemin relatif interne.
|
|
197
|
+
|
|
198
|
+
Une archive dont le contenu **décompressé** dépasse **1 Gio**, ou qui compte plus de
|
|
199
|
+
**100 000 entrées** (fichiers et répertoires), est refusée (code `2`), avant toute
|
|
200
|
+
extraction : les bornes valent pour chaque archive séparément, et ne concernent pas une
|
|
201
|
+
source répertoire, qui n'est pas copiée. `--no-size-limit` les lève toutes les deux, et
|
|
202
|
+
explicitement — en connaissance de cause, le répertoire temporaire devant alors pouvoir
|
|
203
|
+
accueillir l'archive entière.
|
|
204
|
+
|
|
205
|
+
## Utiliser la comparaison depuis Python
|
|
206
|
+
|
|
207
|
+
Le pipeline se décompose en trois étapes indépendantes : **analyse**, **formatage**,
|
|
208
|
+
**écriture**. Le formatage retourne une chaîne — rien n'oblige à l'écrire sur disque,
|
|
209
|
+
elle peut partir directement dans une réponse HTTP ou vers le traitement suivant.
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from pathlib import Path
|
|
213
|
+
|
|
214
|
+
from xml_cmpr import ReportFormat, render, run_comparison, write_report
|
|
215
|
+
|
|
216
|
+
summary = run_comparison(Path("chaine_a"), Path("chaine_b")) # analyse -> RunSummary
|
|
217
|
+
corps = render(summary, ReportFormat.JSON) # formatage -> str
|
|
218
|
+
# ... ou render(summary, "md") pour le Markdown
|
|
219
|
+
|
|
220
|
+
write_report(summary, Path("rapport"), "json") # écriture -> Path (facultatif)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
| Étape | Fonction | Rend |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| analyse | `run_comparison(source_a, source_b, **options)` | `RunSummary` |
|
|
226
|
+
| formatage | `render(summary, "md"\|"json")` | `str` |
|
|
227
|
+
| | `summary.as_dict()` | `dict` (JSON sans passer par la chaîne) |
|
|
228
|
+
| écriture | `write_report(summary, dossier, format)` | `Path` du fichier écrit |
|
|
229
|
+
| | `write_reports(summary, dossier, formats)` | `list[Path]` |
|
|
230
|
+
|
|
231
|
+
`run_comparison` accepte les mêmes options que le CLI (`pattern`, `recursive`,
|
|
232
|
+
`keep_whitespace`, `exclude_xpaths`, `max_diffs_per_file`), et
|
|
233
|
+
`label_a` / `label_b` pour choisir les chemins affichés au rapport. Pour une source ZIP,
|
|
234
|
+
`as_directory(source)` est le context manager qui extrait l'archive. Il applique la même
|
|
235
|
+
bornes que le CLI, `MAX_EXTRACTED_SIZE` (1 Gio) et `MAX_ENTRIES` (100 000) ; `size_limit=`
|
|
236
|
+
(en octets) et `entry_limit=` les changent une à une, et seul un `None` explicite lève la
|
|
237
|
+
borne concernée :
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
from pathlib import Path
|
|
241
|
+
|
|
242
|
+
from xml_cmpr import as_directory, run_comparison
|
|
243
|
+
|
|
244
|
+
with as_directory(Path("chaine_a.zip")) as dir_a:
|
|
245
|
+
summary = run_comparison(dir_a, Path("chaine_b"), label_a=Path("chaine_a.zip"))
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Les exceptions
|
|
249
|
+
|
|
250
|
+
Toutes exportées par `xml_cmpr`, toutes dérivées d'`Exception` :
|
|
251
|
+
|
|
252
|
+
| Exception | Levée par | Cause |
|
|
253
|
+
|---|---|---|
|
|
254
|
+
| `ExcludeError` | `run_comparison`, `Canonicalizer` | un XPath d'exclusion est invalide |
|
|
255
|
+
| `ArchiveError` | `as_directory` | source introuvable, archive illisible, chemin d'entrée suspect |
|
|
256
|
+
| `ArchiveSizeError` | `as_directory` | sous-classe d'`ArchiveError` : archive au-delà de `size_limit` ou d'`entry_limit` (attributs `size`, `limit`, et `measure` : `"bytes"` ou `"entries"`) |
|
|
257
|
+
| `CanonicalError` | `Canonicalizer.load` | un document est illisible ou malformé |
|
|
258
|
+
|
|
259
|
+
La distinction compte pour un appelant qui expose la comparaison : `ExcludeError` et
|
|
260
|
+
`ArchiveError` viennent de ce que l'appelant a fourni — c'est un `400`, pas un `500`
|
|
261
|
+
(`ArchiveSizeError`, attrapée avant, se prête plutôt à un `413`).
|
|
262
|
+
`CanonicalError`, lui, **ne traverse pas `run_comparison`** : la campagne l'attrape
|
|
263
|
+
fichier par fichier et le reporte comme fichier en erreur, sans s'interrompre. On ne le
|
|
264
|
+
voit qu'en appelant `Canonicalizer.load` soi-même.
|
|
265
|
+
|
|
266
|
+
### Exclure des éléments ou attributs de la comparaison
|
|
267
|
+
|
|
268
|
+
`--exclude FICHIER` prend un fichier texte listant un XPath par ligne. Une ligne dont le
|
|
269
|
+
premier caractère non blanc est `#` est un commentaire ; ailleurs dans la ligne, le `#`
|
|
270
|
+
fait partie du XPath — il n'y a pas de commentaire de fin de ligne, justement pour que
|
|
271
|
+
les URI de namespace terminées par `#` restent écrivables.
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
# ignorer l'horodatage de génération et l'identifiant technique
|
|
275
|
+
//DateGeneration
|
|
276
|
+
//Facture/@idTechnique
|
|
277
|
+
//Q{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Chaque élément ou attribut sélectionné par l'un de ces XPath, dans A comme dans B, est
|
|
281
|
+
retiré de la forme canonique **avant** le calcul de l'empreinte : il ne compte ni pour
|
|
282
|
+
le test d'identité, ni dans le diff. Un élément exclu emporte tout son sous-arbre.
|
|
283
|
+
|
|
284
|
+
Le XPath est évalué par Saxon directement sur le document source, sans les préfixes de
|
|
285
|
+
namespace déclarés dans le document (un XPath autonome n'en hérite pas) : pour un nom
|
|
286
|
+
sans espace de noms, écrire `//DateGeneration` ; pour un nom avec espace de noms,
|
|
287
|
+
utiliser la notation `Q{uri}local`, par ex. `//Q{urn:facture}DateGeneration`.
|
|
288
|
+
|
|
289
|
+
## Ce que « identique » veut dire
|
|
290
|
+
|
|
291
|
+
La comparaison porte sur une forme canonique produite par Saxon
|
|
292
|
+
([`normalize.xsl`](https://framagit.org/opikanoba/xml-compare/-/blob/main/src/xml_cmpr/resources/normalize.xsl)). Ne sont **pas** des
|
|
293
|
+
différences :
|
|
294
|
+
|
|
295
|
+
- l'indentation et les retours à la ligne (texte normalisé) ;
|
|
296
|
+
- l'ordre des attributs (triés par nom) ;
|
|
297
|
+
- les préfixes de namespace (les noms sont comparés par URI, en notation de Clark) ;
|
|
298
|
+
- les commentaires et instructions de traitement, **où qu'ils soient** ;
|
|
299
|
+
- la déclaration XML, l'encodage, les sections CDATA.
|
|
300
|
+
|
|
301
|
+
### Commentaires et instructions de traitement
|
|
302
|
+
|
|
303
|
+
Ils ne sont **jamais** comparés, et aucune option ne les réintègre. Ni ceux placés dans
|
|
304
|
+
un élément, ni ceux du prologue — la bannière de génération qu'une chaîne écrit avant
|
|
305
|
+
l'élément racine, la PI `<?xml-stylesheet?>` qui désigne une feuille de style. Deux
|
|
306
|
+
documents qui ne diffèrent que par là sont `identique`.
|
|
307
|
+
|
|
308
|
+
C'est un choix de périmètre, pas une limite d'implémentation : l'outil compare ce que
|
|
309
|
+
les documents *disent* — éléments, attributs, texte — et un commentaire ne dit rien du
|
|
310
|
+
contenu. Deux chaînes de production qui datent et signent différemment leurs sorties ne
|
|
311
|
+
doivent pas produire un rapport plein de différences pour autant. Pour comparer une
|
|
312
|
+
bannière de génération, comparer les fichiers en texte, pas en XML.
|
|
313
|
+
|
|
314
|
+
Sont des différences : l'ordre des éléments frères, la présence ou la valeur d'un
|
|
315
|
+
attribut, le texte d'un élément, la présence d'un élément, et **la position du texte en
|
|
316
|
+
contenu mixte**.
|
|
317
|
+
|
|
318
|
+
Les éléments frères sont appariés d'abord par empreinte de sous-arbre, puis par nom :
|
|
319
|
+
insérer un `<Ligne>` au milieu d'une liste coûte **une** différence, pas un décalage en
|
|
320
|
+
cascade sur tous les suivants. Enfin, deux éléments qui ne diffèrent **que** par leur nom
|
|
321
|
+
(ou leur espace de noms) sont appariés et rapportés en `element_renamed`, à n'importe
|
|
322
|
+
quelle profondeur ; un élément à la fois renommé et modifié reste un `element_removed`
|
|
323
|
+
suivi d'un `element_added`.
|
|
324
|
+
|
|
325
|
+
### Le contenu mixte
|
|
326
|
+
|
|
327
|
+
Quand un élément contient à la fois du texte et des éléments, le texte n'est pas
|
|
328
|
+
agrégé : chaque nœud texte garde sa place parmi les enfants, et se désigne au rapport
|
|
329
|
+
par le segment `#text` (numéroté quand il y en a plusieurs).
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
A : <p>Voir la <ref>note</ref> ci-dessous.</p>
|
|
333
|
+
B : <p>Voir la ci-dessous.<ref>note</ref></p>
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
| Chemin | Type | A | B |
|
|
337
|
+
|---|---|---|---|
|
|
338
|
+
| `/p/#text[1]` | texte modifié | `Voir la` | `Voir la ci-dessous.` |
|
|
339
|
+
| `/p/#text[2]` | texte absent de B | `ci-dessous.` | — |
|
|
340
|
+
|
|
341
|
+
Les deux documents ne disent pas la même chose : ils sont **différents**. Un élément
|
|
342
|
+
qui ne contient que du texte, lui, n'est pas concerné — c'est le cas courant, et son
|
|
343
|
+
texte reste comparé d'un bloc (`text_changed` sur le chemin de l'élément).
|
|
344
|
+
|
|
345
|
+
## Développement
|
|
346
|
+
|
|
347
|
+
```sh
|
|
348
|
+
uv run pytest # tests
|
|
349
|
+
uv run ruff check . && uv run ruff format .
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Publication
|
|
353
|
+
|
|
354
|
+
`publish.sh` enchaîne les étapes d'une livraison sur PyPI : contrôle du verrou, `ruff`,
|
|
355
|
+
`pytest`, construction des distributions, contrôle des métadonnées (`twine check`),
|
|
356
|
+
vérification que la roue embarque bien `normalize.xsl`, puis téléversement. Il refuse un
|
|
357
|
+
arbre de travail modifié ou une version déjà publiée, et ne pose aucun tag lui-même — il
|
|
358
|
+
rappelle la commande à la fin.
|
|
359
|
+
|
|
360
|
+
```sh
|
|
361
|
+
uv version --bump patch # 0.2.0 -> 0.2.1 (ou minor / major)
|
|
362
|
+
./publish.sh --test # répétition générale sur TestPyPI
|
|
363
|
+
./publish.sh # publication réelle sur PyPI
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
| Option | Effet |
|
|
367
|
+
|---|---|
|
|
368
|
+
| `--test` | publier sur TestPyPI (index `testpypi` déclaré dans `pyproject.toml`) |
|
|
369
|
+
| `--dry-run` | tout exécuter sauf le téléversement |
|
|
370
|
+
| `--skip-checks` | ne pas rejouer `ruff` et `pytest` |
|
|
371
|
+
| `--allow-dirty` | autoriser un arbre de travail modifié |
|
|
372
|
+
|
|
373
|
+
Le jeton d'API s'obtient, par ordre de préférence :
|
|
374
|
+
|
|
375
|
+
1. par une commande, `PUBLISH_TOKEN_CMD='pass show pypi/xml-cmpr' ./publish.sh` — le
|
|
376
|
+
jeton ne transite ni par l'historique du shell ni par l'environnement ;
|
|
377
|
+
2. par saisie au clavier, sans écho, si rien n'est fourni et que le script tourne dans
|
|
378
|
+
un terminal ;
|
|
379
|
+
3. par `UV_PUBLISH_TOKEN`, toléré pour la compatibilité mais déconseillé : exporté dans
|
|
380
|
+
le profil, il est lisible par tous les processus de la session.
|
|
381
|
+
|
|
382
|
+
Quelle que soit sa source, le script retire le jeton de l'environnement dès son
|
|
383
|
+
démarrage et ne le remet qu'à `uv publish` : `pytest`, `uv build` et `twine` — donc les
|
|
384
|
+
dépendances de dev et les hooks de build — ne le voient jamais. Après la première
|
|
385
|
+
publication, remplacer le jeton global du compte par un jeton limité au projet.
|
|
386
|
+
|
|
387
|
+
Le jeton est celui du dépôt visé — un jeton PyPI ne vaut pas sur TestPyPI, et
|
|
388
|
+
réciproquement. Après un `--test`, l'installation se vérifie par
|
|
389
|
+
`uv tool install --index https://test.pypi.org/simple xml-cmpr`.
|