python-semantic-release 9.17.0__py3-none-any.whl → 9.18.0__py3-none-any.whl

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.
Files changed (34) hide show
  1. {python_semantic_release-9.17.0.dist-info → python_semantic_release-9.18.0.dist-info}/METADATA +1 -1
  2. {python_semantic_release-9.17.0.dist-info → python_semantic_release-9.18.0.dist-info}/RECORD +34 -34
  3. semantic_release/__init__.py +1 -1
  4. semantic_release/changelog/context.py +17 -1
  5. semantic_release/changelog/release_history.py +39 -5
  6. semantic_release/cli/changelog_writer.py +4 -0
  7. semantic_release/cli/commands/changelog.py +45 -1
  8. semantic_release/cli/commands/version.py +20 -2
  9. semantic_release/cli/config.py +19 -2
  10. semantic_release/commit_parser/angular.py +33 -13
  11. semantic_release/commit_parser/emoji.py +34 -12
  12. semantic_release/commit_parser/token.py +19 -6
  13. semantic_release/commit_parser/util.py +11 -3
  14. semantic_release/const.py +2 -0
  15. semantic_release/data/templates/angular/md/.components/changes.md.j2 +59 -14
  16. semantic_release/data/templates/angular/md/.components/first_release.md.j2 +11 -4
  17. semantic_release/data/templates/angular/md/.components/macros.md.j2 +66 -0
  18. semantic_release/data/templates/angular/md/.components/versioned_changes.md.j2 +13 -7
  19. semantic_release/data/templates/angular/md/.release_notes.md.j2 +25 -21
  20. semantic_release/data/templates/angular/rst/.components/changes.rst.j2 +68 -23
  21. semantic_release/data/templates/angular/rst/.components/first_release.rst.j2 +2 -0
  22. semantic_release/data/templates/angular/rst/.components/macros.rst.j2 +66 -0
  23. semantic_release/data/templates/angular/rst/.components/versioned_changes.rst.j2 +2 -0
  24. semantic_release/helpers.py +8 -1
  25. semantic_release/hvcs/bitbucket.py +15 -0
  26. semantic_release/hvcs/gitea.py +21 -0
  27. semantic_release/hvcs/github.py +21 -0
  28. semantic_release/hvcs/gitlab.py +20 -0
  29. semantic_release/version/algorithm.py +48 -27
  30. {python_semantic_release-9.17.0.dist-info → python_semantic_release-9.18.0.dist-info}/AUTHORS.rst +0 -0
  31. {python_semantic_release-9.17.0.dist-info → python_semantic_release-9.18.0.dist-info}/LICENSE +0 -0
  32. {python_semantic_release-9.17.0.dist-info → python_semantic_release-9.18.0.dist-info}/WHEEL +0 -0
  33. {python_semantic_release-9.17.0.dist-info → python_semantic_release-9.18.0.dist-info}/entry_points.txt +0 -0
  34. {python_semantic_release-9.17.0.dist-info → python_semantic_release-9.18.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-semantic-release
3
- Version: 9.17.0
3
+ Version: 9.18.0
4
4
  Summary: Automatic Semantic Versioning for Python projects
5
5
  Author-email: Rolf Erik Lekang <me@rolflekang.com>
6
6
  License: MIT
@@ -1,75 +1,75 @@
1
- semantic_release/__init__.py,sha256=HGRoymlde9dCDLCL2q3_ispJjvMuGYmdsAyWsLPpHfM,1229
1
+ semantic_release/__init__.py,sha256=-F25Nu3RzbeFLRgFP4ms_VHphEUTK1SdYz0y3LLOJB8,1229
2
2
  semantic_release/__main__.py,sha256=KOIBOvLruqfi5ArXcWK3ucIZ7NB55kfCbycJaxx6aQg,1485
3
- semantic_release/const.py,sha256=Z1o2QNh60wSLeF-_1TemMBjU3ZXbV0XghnUFsbTVfOs,831
3
+ semantic_release/const.py,sha256=wInJR7vcOgT1ysm5VuJQ6lD_ZGYnCwRVKz7Uz3htQc4,861
4
4
  semantic_release/enums.py,sha256=vrEw1UNRcNrFjPqOFnuUzfeoqKj0ChixVVlyk5fqbng,1744
5
5
  semantic_release/errors.py,sha256=PY9rmviSFBZkqawW6VXbUfmF9C_RNOIObcmeGxLefMo,2904
6
6
  semantic_release/gitproject.py,sha256=G4XrucN-ZwT1Kj4RMrABcr1vWb0bjKgurEeJjcL-61c,9422
7
7
  semantic_release/globals.py,sha256=imI9WKGa6MS2pTRAZiWZ2qIJup2eWnBz3OZmIj2YIHM,158
8
- semantic_release/helpers.py,sha256=T7gEYx9npipoV1-lNc0n-_vwVbdVqeREpQ4mv66auTE,9529
8
+ semantic_release/helpers.py,sha256=8yQTYUS3InvEnEqqhzJPM_R-69Pk6k9gF1NgPlgQx1Q,9774
9
9
  semantic_release/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  semantic_release/changelog/__init__.py,sha256=Bg6Xe5Vt32rWoMscW-hd4sUwiZqzWmsg4CD1EhMesMY,262
11
- semantic_release/changelog/context.py,sha256=TcBG-fVmYuxl7uD-AtsHVVE5Md5KwCRGJQxbE3dJdTc,5459
12
- semantic_release/changelog/release_history.py,sha256=d_n35sJHFJHgni3-xKXfkCoMiSZLy3ChLU5e9Tn5g4c,9267
11
+ semantic_release/changelog/context.py,sha256=WeLQ2BvYEWunIF8XEl6ldQE4IRg0d7r8mYSBi-TqK7o,5988
12
+ semantic_release/changelog/release_history.py,sha256=epxN_tFGk3H3OGjiMsvwEVp5wgrTIKn2WRmjK5ZJ6SM,10578
13
13
  semantic_release/changelog/template.py,sha256=R3V5m-7kv9ES23e_g37fe17tk-ESgvwV0C9rp5uoeOE,5691
14
14
  semantic_release/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- semantic_release/cli/changelog_writer.py,sha256=S91en98KrmXMA0AUwqsWa8zGY1i2-LRVFBcK0jWqfPw,9168
15
+ semantic_release/cli/changelog_writer.py,sha256=92DntVFFGdzFc5eBUgrRXbSOSV9KVvV769NyhLOTirg,9281
16
16
  semantic_release/cli/cli_context.py,sha256=Nop71LdVCJOeSUHgTXunMyK3xAu_QKQC2cRp1QBVkX0,4134
17
- semantic_release/cli/config.py,sha256=aYquSXNdjhF45BvqcdjaQSu1DcRJeugifOvpMyknwrU,32337
17
+ semantic_release/cli/config.py,sha256=rLva6QIB5DJIYtWCnC1GHiHMWbnEkWg_YxIVVhznjnk,33138
18
18
  semantic_release/cli/const.py,sha256=h7XE2D0D__TAZSrUUtVszwvzpkHTMOiQCf97XQNbEvA,163
19
19
  semantic_release/cli/github_actions_output.py,sha256=6oNwjnQBg9XF5QgGc4TgbwX_-W0aj65VwGSL4ALvqVg,2296
20
20
  semantic_release/cli/masking_filter.py,sha256=ric34rnXfN5RiAVVaKnhiMJOxTnEl26kI06jQqZPZoQ,3072
21
21
  semantic_release/cli/util.py,sha256=FyXaBkeL7nXKjy3X9rQLEwvn7p46xPekp2V8Z-5MVrk,3755
22
22
  semantic_release/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- semantic_release/cli/commands/changelog.py,sha256=w_sXuFJHIqcueBQdeNeWn6fqbLVPPl_c-dEhv3Pb_BA,3870
23
+ semantic_release/cli/commands/changelog.py,sha256=i3j6jhcfhFo3d7Bgrp1tD01vJkuTZvtaWX7bPteLQbA,5345
24
24
  semantic_release/cli/commands/generate_config.py,sha256=2xZOu3NpyhBp0pWr7d8ugKl_kjqQgpSsSMHq5wHTfrE,1699
25
25
  semantic_release/cli/commands/main.py,sha256=237rn_Od4LOWfjUjiUKI_jSV820MfcCtRpwPjxjLbyU,4312
26
26
  semantic_release/cli/commands/publish.py,sha256=y_LalPti_kZeQJzl2CR2pTZUK8DCMvNSTe4NaMC5TJA,2875
27
- semantic_release/cli/commands/version.py,sha256=y8m8ZyJe9vGhNsl5El2a4QwNM-upTRHfJS2T9zJoFWc,24824
27
+ semantic_release/cli/commands/version.py,sha256=BMAOXToqOC_U0nh3d-QszAOS7xhN86aGhFiaePSdfws,25287
28
28
  semantic_release/commit_parser/__init__.py,sha256=cv5HFBdw7OJd4Laj4Ex8ZZ5Tml8GwXgQcXW6Pasr2Ao,615
29
29
  semantic_release/commit_parser/_base.py,sha256=oDifeTmFDpS238cp_DDrGzfidaKeAD5olCB5IM4Q6z8,3058
30
- semantic_release/commit_parser/angular.py,sha256=tJqc9A-XJs85FH1EiDgm-k3_ryhPp8ln0EbrkRZkoWo,18367
31
- semantic_release/commit_parser/emoji.py,sha256=zXlQUwTawMq9G7Uf5nriNQJv_tXFDev9UG3CHT3_Jvs,17283
30
+ semantic_release/commit_parser/angular.py,sha256=UM88ethWT34__kDgsBXXb26d0V8FN1ZFjcU7psuOhgY,19259
31
+ semantic_release/commit_parser/emoji.py,sha256=T3kw8jyq1zYME3f6JJ6O3hVJ2IpRk3DD5LB0VOlcbMM,18194
32
32
  semantic_release/commit_parser/scipy.py,sha256=0rYZglJ7uib-1Deu4J30wHh7AZS8KfO0eND82bPtDQ8,4526
33
33
  semantic_release/commit_parser/tag.py,sha256=oGB3lgyp2Eu3Tg3jjxqNzN86N6bokSaFu6f4Ir6IS_k,3546
34
- semantic_release/commit_parser/token.py,sha256=RXdoCmKMTKMmUJGEUvfCiAOCPRrV0WubXojwa6FbvFg,7363
35
- semantic_release/commit_parser/util.py,sha256=9diralZ8QbMOMpVEp9MtrMVdkGsD-fS1F4cmy6eyysU,3674
36
- semantic_release/data/templates/angular/md/.release_notes.md.j2,sha256=BzpatS7WYBcpHii8qiDemyI1ygXM6Q1nbxdcdcps__U,2107
34
+ semantic_release/commit_parser/token.py,sha256=ECgi7eeSgk3Biq1Y_ChbFJZQLkrUpNvGhIaEOXrNC4M,7904
35
+ semantic_release/commit_parser/util.py,sha256=_ACiopznjwINn4t1zPHl8bxZEc0zOAURTycNU-sXs3M,3959
36
+ semantic_release/data/templates/angular/md/.release_notes.md.j2,sha256=3d4ihVVY1u2rAm7XgydRKQq92YfQkFWanpEP5uQ8xmM,2512
37
37
  semantic_release/data/templates/angular/md/CHANGELOG.md.j2,sha256=FZmrQ-qOIoSoJmAa_NFaRelfmqUpypU2xlDeScdGOf4,729
38
38
  semantic_release/data/templates/angular/md/.components/changelog_header.md.j2,sha256=qNxTuSr59CV_yyimVU_RYp5azCnK0l6nJ03Zf0u5Ugg,166
39
39
  semantic_release/data/templates/angular/md/.components/changelog_init.md.j2,sha256=MyEQdWUemGXKWDhvpTmubZdozd3iaLECZAI1VRlE7mg,862
40
40
  semantic_release/data/templates/angular/md/.components/changelog_update.md.j2,sha256=uVF4wbbjTMvl6Kbsq9xy3YIrj-uhBnHylEfA7S76aHI,2606
41
- semantic_release/data/templates/angular/md/.components/changes.md.j2,sha256=AnF6sEsdaOeCS6E0_Q1PS430jufC1wg4ZMIJdMuBuNI,4000
42
- semantic_release/data/templates/angular/md/.components/first_release.md.j2,sha256=jMUZiLwhMqAOjdYOCphJxJr8C411cKRzhsabyebj_AY,162
43
- semantic_release/data/templates/angular/md/.components/macros.md.j2,sha256=GEql_lFHWqtfZUMYo8WT3E_YOTf0gvGhjqRQw0c7mlk,5890
41
+ semantic_release/data/templates/angular/md/.components/changes.md.j2,sha256=NIdXNNao4Oq9nWHaq6X4ZbGMMJ2H-7yxqAP5HmBmSV8,5965
42
+ semantic_release/data/templates/angular/md/.components/first_release.md.j2,sha256=-S2aJV3ka4YicLs8UF6BEV-CDnL8iXLcNRRIiENFrYE,421
43
+ semantic_release/data/templates/angular/md/.components/macros.md.j2,sha256=RzBVwKSCb-WmRIMFVQL9U157mAih-PzZ_iRcfyWkG20,8196
44
44
  semantic_release/data/templates/angular/md/.components/unreleased_changes.md.j2,sha256=HRLj6cyRfPZXC0s-0Av6s0Gp3jKxWg9AIEtIXBVqJuY,177
45
- semantic_release/data/templates/angular/md/.components/versioned_changes.md.j2,sha256=2Hky2mBrC4jltz3mvaiPDD0KQP0ELe5Ag75HgaLpaIE,257
45
+ semantic_release/data/templates/angular/md/.components/versioned_changes.md.j2,sha256=KbU7D_n_VqET7q2CLwBQ7931dvO7xQklADw-oY6ujlY,502
46
46
  semantic_release/data/templates/angular/rst/CHANGELOG.rst.j2,sha256=VmkXEMHiPBdZ1Z47QMxnJBZA0NbFSbKenUbThQVFAGY,731
47
47
  semantic_release/data/templates/angular/rst/.components/changelog_header.rst.j2,sha256=c9xN1SEYLFwMvPYXYKt-ZbYPn2-Ss0V7zepEtFFj3Os,200
48
48
  semantic_release/data/templates/angular/rst/.components/changelog_init.rst.j2,sha256=XD0l3eTyz1yydLKsmSqBk-u8RnO-RdQ2Q8uWezHMAWw,866
49
49
  semantic_release/data/templates/angular/rst/.components/changelog_update.rst.j2,sha256=x23-qk9owJrOQaHx8SgSnIZECITjPf1R2awfv9EOHN0,2604
50
- semantic_release/data/templates/angular/rst/.components/changes.rst.j2,sha256=HwuxpKLcRZiMZZafESIfy0zjleAwxrnMV9tRgaF3Mwc,5353
51
- semantic_release/data/templates/angular/rst/.components/first_release.rst.j2,sha256=tChYsdSFMaB0DU9KROYSxijdb7tqaN1psKmDMbrVrJw,443
52
- semantic_release/data/templates/angular/rst/.components/macros.rst.j2,sha256=luNkkvsWeaUx_xQ-D28USFS_Q56kzJcUXiIX2umBK9I,6790
50
+ semantic_release/data/templates/angular/rst/.components/changes.rst.j2,sha256=IZdUJ5tt-8P5AyPgS1hsTuu2vg10GDINJ9wLJ4CpP68,7352
51
+ semantic_release/data/templates/angular/rst/.components/first_release.rst.j2,sha256=huaO-B3BRs7h2LRks2a6-656W2qzURkzLiyuKvYxMTg,462
52
+ semantic_release/data/templates/angular/rst/.components/macros.rst.j2,sha256=WLNUD2H2V-5vrwT7TKelwQ2wclLcZxFs0E2Lk3Ld10U,9096
53
53
  semantic_release/data/templates/angular/rst/.components/unreleased_changes.rst.j2,sha256=ARBhc1ZpKwehGKDvOMqukmN59mTJiHzHsS7rOfKYCt8,202
54
- semantic_release/data/templates/angular/rst/.components/versioned_changes.rst.j2,sha256=NZfn1W14QochiAJ43oNKmcrCn_vgfbkKtvOTAw1jEc8,530
54
+ semantic_release/data/templates/angular/rst/.components/versioned_changes.rst.j2,sha256=5THfdfYRjHNEQZVUDt112124hFEZxu7J4qIdgn54O1Q,553
55
55
  semantic_release/hvcs/__init__.py,sha256=JwoaLOF-12L-OBo_9-tOXXhdiHKeVungA9865to2oZk,494
56
56
  semantic_release/hvcs/_base.py,sha256=ycHg0WljEVTqjFXoGRrHMvbyqWqPs4HziB090IcPZSE,2664
57
- semantic_release/hvcs/bitbucket.py,sha256=Hzk27OOcpedFKVX4z3cY8DIUNNxe6D57J0d3sWNPIOY,9687
58
- semantic_release/hvcs/gitea.py,sha256=YTpAnJ4bBdMjIhgAvVMF8vPZ6O07IgWuXeqTbEU-GNE,12715
59
- semantic_release/hvcs/github.py,sha256=PLqbXDpHvXqE-uY8dmv7MuyjjnkMv0Va0vXo3UQUIUA,19937
60
- semantic_release/hvcs/gitlab.py,sha256=6BrhI5k7x5YKKRc9NxvLKEoKq7g3dnpDd2WDe8tuVFs,9964
57
+ semantic_release/hvcs/bitbucket.py,sha256=5Z6q_ySwLZ-repf5EH0e8VVvk4fkWvdKCp3sIw4cBBs,10162
58
+ semantic_release/hvcs/gitea.py,sha256=wETF7JDR2xmcyR355faVx7GAtee35gdx7u27Si0prz8,13423
59
+ semantic_release/hvcs/github.py,sha256=nks7cEyp9usw8e-aZ6V2SXsDHu6nPk5haM92LkQ8UBk,20649
60
+ semantic_release/hvcs/gitlab.py,sha256=bKRIzzdAcFmXT_PixadSbpKn6icfLu8KQ7HIHc_uKkU,10623
61
61
  semantic_release/hvcs/remote_hvcs_base.py,sha256=cV8qYHtP47bmfIZqV4K2EiMHskFEoIo6agW_iU6wfVE,6105
62
62
  semantic_release/hvcs/token_auth.py,sha256=ZjT56-NIPB4OKIt1qwHCu1TavXnrWFIBl9ARlg56hgU,663
63
63
  semantic_release/hvcs/util.py,sha256=guxisysY_IW5tv7aaV-iVPEVJzgbOs375kiRRpSquTI,2879
64
64
  semantic_release/version/__init__.py,sha256=CLhtGQry9dLIij5XyRa9ZevxU_1p8tjMTSQ-K_GMpWM,270
65
- semantic_release/version/algorithm.py,sha256=-ppIqvvcAvc4nMtsClNNEzpEHWzgv2oKFydMW6ium6M,15941
65
+ semantic_release/version/algorithm.py,sha256=s5lso4Py-PiVzuPhgeJOz6IDzIdqc6EeoUirmBZ8IXY,16696
66
66
  semantic_release/version/declaration.py,sha256=f6Ld7hIhrqvDrRBapJHr-KDimuyo-4IG8009Zu9BIgU,7357
67
67
  semantic_release/version/translator.py,sha256=P1noIsVBn8u6zNOFjG0xKYOWapxqf_PHSMvMeLJ9kXg,3050
68
68
  semantic_release/version/version.py,sha256=6PCtSbLP88U1daoxnCwHc--YguZo4waGNLqJ5JfeczE,14175
69
- python_semantic_release-9.17.0.dist-info/AUTHORS.rst,sha256=XOReVvpymEFUPsS2QPH97jlfJBVrxwS2eu8-jVAe4gk,230
70
- python_semantic_release-9.17.0.dist-info/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
71
- python_semantic_release-9.17.0.dist-info/METADATA,sha256=1xRK6C1aw6HjxmCa963whjgtiQPjhbOcSW0vhaghQ_M,3812
72
- python_semantic_release-9.17.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
73
- python_semantic_release-9.17.0.dist-info/entry_points.txt,sha256=r2Jql3GTQyugQnvf34l2eXk1O_Qx6llR_xixG1ZWgD0,105
74
- python_semantic_release-9.17.0.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
75
- python_semantic_release-9.17.0.dist-info/RECORD,,
69
+ python_semantic_release-9.18.0.dist-info/AUTHORS.rst,sha256=XOReVvpymEFUPsS2QPH97jlfJBVrxwS2eu8-jVAe4gk,230
70
+ python_semantic_release-9.18.0.dist-info/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
71
+ python_semantic_release-9.18.0.dist-info/METADATA,sha256=G6MEURvvUmQP39A6ksc-bSvIS4XeKxbP3iFNDF9gytE,3812
72
+ python_semantic_release-9.18.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
73
+ python_semantic_release-9.18.0.dist-info/entry_points.txt,sha256=r2Jql3GTQyugQnvf34l2eXk1O_Qx6llR_xixG1ZWgD0,105
74
+ python_semantic_release-9.18.0.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
75
+ python_semantic_release-9.18.0.dist-info/RECORD,,
@@ -24,7 +24,7 @@ from semantic_release.version import (
24
24
  tags_and_versions,
25
25
  )
26
26
 
27
- __version__ = "9.17.0"
27
+ __version__ = "9.18.0"
28
28
 
29
29
  __all__ = [
30
30
  "CommitParser",
@@ -4,10 +4,13 @@ import logging
4
4
  import os
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum
7
- from pathlib import Path
7
+ from pathlib import Path, PurePosixPath
8
8
  from re import compile as regexp
9
9
  from typing import TYPE_CHECKING, Any, Callable, Literal
10
10
 
11
+ from urllib3.util import Url
12
+
13
+ from semantic_release.const import PYPI_WEB_DOMAIN
11
14
  from semantic_release.helpers import sort_numerically
12
15
 
13
16
  if TYPE_CHECKING: # pragma: no cover
@@ -26,6 +29,7 @@ class ReleaseNotesContext:
26
29
  version: Version
27
30
  release: Release
28
31
  mask_initial_release: bool
32
+ license_name: str
29
33
  filters: tuple[Callable[..., Any], ...] = ()
30
34
 
31
35
  def bind_to_environment(self, env: Environment) -> Environment:
@@ -86,6 +90,7 @@ def make_changelog_context(
86
90
  hvcs_type=hvcs_client.__class__.__name__.lower(),
87
91
  filters=(
88
92
  *hvcs_client.get_changelog_context_filters(),
93
+ create_pypi_url,
89
94
  read_file,
90
95
  convert_md_to_rst,
91
96
  autofit_text_width,
@@ -94,6 +99,17 @@ def make_changelog_context(
94
99
  )
95
100
 
96
101
 
102
+ def create_pypi_url(package_name: str, version: str = "") -> str:
103
+ project_name = package_name.strip("/").strip()
104
+ if not project_name:
105
+ raise ValueError("package_name must not be empty!")
106
+ return Url(
107
+ scheme="https",
108
+ host=PYPI_WEB_DOMAIN,
109
+ path=str(PurePosixPath("project", project_name, version.strip("/").strip())),
110
+ ).url.rstrip("/")
111
+
112
+
97
113
  def read_file(filepath: str) -> str:
98
114
  try:
99
115
  if not filepath:
@@ -9,7 +9,9 @@ from git.objects.tag import TagObject
9
9
 
10
10
  from semantic_release.commit_parser import ParseError
11
11
  from semantic_release.commit_parser.token import ParsedCommit
12
+ from semantic_release.commit_parser.util import force_str
12
13
  from semantic_release.enums import LevelBump
14
+ from semantic_release.helpers import validate_types_in_sequence
13
15
  from semantic_release.version.algorithm import tags_and_versions
14
16
 
15
17
  if TYPE_CHECKING: # pragma: no cover
@@ -50,6 +52,12 @@ class ReleaseHistory:
50
52
  for tag, version in all_git_tags_and_versions
51
53
  }
52
54
 
55
+ ignore_merge_commits = bool(
56
+ hasattr(commit_parser, "options")
57
+ and hasattr(commit_parser.options, "ignore_merge_commits")
58
+ and getattr(commit_parser.options, "ignore_merge_commits") # noqa: B009
59
+ )
60
+
53
61
  # Strategy:
54
62
  # Loop through commits in history, parsing as we go.
55
63
  # Add these commits to `unreleased` as a key-value mapping
@@ -110,14 +118,36 @@ class ReleaseHistory:
110
118
  # returns a ParseResult or list of ParseResult objects,
111
119
  # it is usually one, but we split a commit if a squashed merge is detected
112
120
  parse_results = commit_parser.parse(commit)
113
- if not isinstance(parse_results, list):
114
- parse_results = [parse_results]
115
121
 
116
- is_squash_commit = bool(len(parse_results) > 1)
122
+ if not any(
123
+ (
124
+ isinstance(parse_results, (ParseError, ParsedCommit)),
125
+ (
126
+ (
127
+ isinstance(parse_results, list)
128
+ or type(parse_results) == tuple
129
+ )
130
+ and validate_types_in_sequence(
131
+ parse_results, (ParseError, ParsedCommit)
132
+ )
133
+ ),
134
+ )
135
+ ):
136
+ raise TypeError("Unexpected type returned from commit_parser.parse")
137
+
138
+ results: list[ParseResult] = [
139
+ *(
140
+ [parse_results]
141
+ if isinstance(parse_results, (ParseError, ParsedCommit))
142
+ else parse_results
143
+ ),
144
+ ]
145
+
146
+ is_squash_commit = bool(len(results) > 1)
117
147
 
118
148
  # iterate through parsed commits to add to changelog definition
119
- for parsed_result in parse_results:
120
- commit_message = str(parsed_result.commit.message)
149
+ for parsed_result in results:
150
+ commit_message = force_str(parsed_result.commit.message)
121
151
  commit_type = (
122
152
  "unknown"
123
153
  if isinstance(parsed_result, ParseError)
@@ -135,6 +165,10 @@ class ReleaseHistory:
135
165
  else parsed_result.bump
136
166
  )
137
167
 
168
+ if ignore_merge_commits and parsed_result.is_merge_commit():
169
+ log.info("Excluding merge commit[%s]", parsed_result.short_hash)
170
+ continue
171
+
138
172
  # Skip excluded commits except for any commit causing a version bump
139
173
  # Reasoning: if a commit causes a version bump, and no other commits
140
174
  # are included, then the changelog will be empty. Even if ther was other
@@ -13,6 +13,7 @@ import semantic_release
13
13
  from semantic_release.changelog.context import (
14
14
  ReleaseNotesContext,
15
15
  autofit_text_width,
16
+ create_pypi_url,
16
17
  make_changelog_context,
17
18
  )
18
19
  from semantic_release.changelog.template import environment, recursive_render
@@ -229,6 +230,7 @@ def generate_release_notes(
229
230
  history: ReleaseHistory,
230
231
  style: str,
231
232
  mask_initial_release: bool,
233
+ license_name: str = "",
232
234
  ) -> str:
233
235
  users_tpl_file = template_dir / DEFAULT_RELEASE_NOTES_TPL_FILE
234
236
 
@@ -255,8 +257,10 @@ def generate_release_notes(
255
257
  version=release["version"],
256
258
  release=release,
257
259
  mask_initial_release=mask_initial_release,
260
+ license_name=license_name,
258
261
  filters=(
259
262
  *hvcs_client.get_changelog_context_filters(),
263
+ create_pypi_url,
260
264
  autofit_text_width,
261
265
  sort_numerically,
262
266
  ),
@@ -1,10 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ from contextlib import suppress
5
+ from pathlib import Path
4
6
  from typing import TYPE_CHECKING
5
7
 
6
8
  import click
7
- from git import Repo
9
+ import tomlkit
10
+ from git import GitCommandError, Repo
8
11
 
9
12
  from semantic_release.changelog.release_history import ReleaseHistory
10
13
  from semantic_release.cli.changelog_writer import (
@@ -21,6 +24,43 @@ if TYPE_CHECKING: # pragma: no cover
21
24
  log = logging.getLogger(__name__)
22
25
 
23
26
 
27
+ def get_license_name_for_release(tag_name: str, project_root: Path) -> str:
28
+ # Retrieve the license name at the time of the specific release tag
29
+ project_metadata: dict[str, str] = {}
30
+ curr_dir = Path.cwd().resolve()
31
+ allowed_directories = [
32
+ dir_path
33
+ for dir_path in [curr_dir, *curr_dir.parents]
34
+ if str(project_root) in str(dir_path)
35
+ ]
36
+ for allowed_dir in allowed_directories:
37
+ proj_toml = allowed_dir.joinpath("pyproject.toml")
38
+ with Repo(project_root) as git_repo, suppress(GitCommandError):
39
+ toml_contents = git_repo.git.show(
40
+ f"{tag_name}:{proj_toml.relative_to(project_root)}"
41
+ )
42
+ config_toml = tomlkit.parse(toml_contents)
43
+ project_metadata = config_toml.unwrap().get("project", project_metadata)
44
+ break
45
+
46
+ license_cfg = project_metadata.get(
47
+ "license-expression",
48
+ project_metadata.get(
49
+ "license",
50
+ "",
51
+ ),
52
+ )
53
+
54
+ if not isinstance(license_cfg, (str, dict)) or license_cfg is None:
55
+ return ""
56
+
57
+ return (
58
+ license_cfg.get("text", "") # type: ignore[attr-defined]
59
+ if isinstance(license_cfg, dict)
60
+ else license_cfg or ""
61
+ )
62
+
63
+
24
64
  def post_release_notes(
25
65
  release_tag: str,
26
66
  release_notes: str,
@@ -120,6 +160,10 @@ def changelog(cli_ctx: CliContextObj, release_tag: str | None) -> None:
120
160
  release_history,
121
161
  style=runtime.changelog_style,
122
162
  mask_initial_release=runtime.changelog_mask_initial_release,
163
+ license_name=get_license_name_for_release(
164
+ tag_name=release_tag,
165
+ project_root=runtime.repo_dir,
166
+ ),
123
167
  )
124
168
 
125
169
  try:
@@ -699,13 +699,31 @@ def version( # noqa: C901
699
699
  log.info("Remote does not support releases. Skipping release creation...")
700
700
  return
701
701
 
702
+ license_cfg = runtime.project_metadata.get(
703
+ "license-expression",
704
+ runtime.project_metadata.get(
705
+ "license",
706
+ "",
707
+ ),
708
+ )
709
+
710
+ if not isinstance(license_cfg, (str, dict)) or license_cfg is None:
711
+ license_cfg = ""
712
+
713
+ license_name = (
714
+ license_cfg.get("text", "")
715
+ if isinstance(license_cfg, dict)
716
+ else license_cfg or ""
717
+ )
718
+
702
719
  release_notes = generate_release_notes(
703
720
  hvcs_client,
704
- release_history.released[new_version],
705
- runtime.template_dir,
721
+ release=release_history.released[new_version],
722
+ template_dir=runtime.template_dir,
706
723
  history=release_history,
707
724
  style=runtime.changelog_style,
708
725
  mask_initial_release=runtime.changelog_mask_initial_release,
726
+ license_name=license_name,
709
727
  )
710
728
 
711
729
  exception: Exception | None = None
@@ -15,6 +15,8 @@ from re import (
15
15
  )
16
16
  from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Type, Union
17
17
 
18
+ # typing_extensions is for Python 3.8, 3.9, 3.10 compatibility
19
+ import tomlkit
18
20
  from git import Actor, InvalidGitRepositoryError
19
21
  from git.repo.base import Repo
20
22
  from jinja2 import Environment
@@ -26,8 +28,6 @@ from pydantic import (
26
28
  field_validator,
27
29
  model_validator,
28
30
  )
29
-
30
- # typing_extensions is for Python 3.8, 3.9, 3.10 compatibility
31
31
  from typing_extensions import Annotated, Self
32
32
  from urllib3.util.url import parse_url
33
33
 
@@ -523,6 +523,7 @@ def _recursive_getattr(obj: Any, path: str) -> Any:
523
523
  class RuntimeContext:
524
524
  _mask_attrs_: ClassVar[List[str]] = ["hvcs_client.token"]
525
525
 
526
+ project_metadata: dict[str, Any]
526
527
  repo_dir: Path
527
528
  commit_parser: CommitParser[ParseResult, ParserOptions]
528
529
  version_translator: VersionTranslator
@@ -599,6 +600,21 @@ class RuntimeContext:
599
600
  # credentials masking for logging
600
601
  masker = MaskingFilter(_use_named_masks=raw.logging_use_named_masks)
601
602
 
603
+ # TODO: move to config if we change how the generated config is constructed
604
+ # Retrieve project metadata from pyproject.toml
605
+ project_metadata: dict[str, str] = {}
606
+ curr_dir = Path.cwd().resolve()
607
+ allowed_directories = [
608
+ dir_path
609
+ for dir_path in [curr_dir, *curr_dir.parents]
610
+ if str(raw.repo_dir) in str(dir_path)
611
+ ]
612
+ for allowed_dir in allowed_directories:
613
+ if (proj_toml := allowed_dir.joinpath("pyproject.toml")).exists():
614
+ config_toml = tomlkit.parse(proj_toml.read_text())
615
+ project_metadata = config_toml.unwrap().get("project", project_metadata)
616
+ break
617
+
602
618
  # Retrieve details from repository
603
619
  with Repo(str(raw.repo_dir)) as git_repo:
604
620
  try:
@@ -825,6 +841,7 @@ class RuntimeContext:
825
841
  # )
826
842
 
827
843
  self = cls(
844
+ project_metadata=project_metadata,
828
845
  repo_dir=raw.repo_dir,
829
846
  commit_parser=commit_parser,
830
847
  version_translator=version_translator,
@@ -101,6 +101,10 @@ class AngularParserOptions(ParserOptions):
101
101
  parse_squash_commits: bool = False
102
102
  """Toggle flag for whether or not to parse squash commits"""
103
103
 
104
+ # TODO: breaking change v10, change default to True
105
+ ignore_merge_commits: bool = False
106
+ """Toggle flag for whether or not to ignore merge commits"""
107
+
104
108
  @property
105
109
  def tag_to_level(self) -> dict[str, LevelBump]:
106
110
  """A mapping of commit tags to the level bump they should result in."""
@@ -188,6 +192,7 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
188
192
  ),
189
193
  flags=re.MULTILINE | re.IGNORECASE,
190
194
  )
195
+ self.notice_selector = regexp(r"^NOTICE: (?P<notice>.+)$")
191
196
  self.filters = {
192
197
  "typo-extra-spaces": (regexp(r"(\S) +(\S)"), r"\1 \2"),
193
198
  "git-header-commit": (
@@ -232,11 +237,18 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
232
237
  def commit_body_components_separator(
233
238
  self, accumulator: dict[str, list[str]], text: str
234
239
  ) -> dict[str, list[str]]:
235
- if match := breaking_re.match(text):
236
- accumulator["breaking_descriptions"].append(match.group(1) or "")
240
+ if (match := breaking_re.match(text)) and (brk_desc := match.group(1)):
241
+ accumulator["breaking_descriptions"].append(brk_desc)
237
242
  # TODO: breaking change v10, removes breaking change footers from descriptions
238
243
  # return accumulator
239
244
 
245
+ elif (match := self.notice_selector.match(text)) and (
246
+ notice := match.group("notice")
247
+ ):
248
+ accumulator["notices"].append(notice)
249
+ # TODO: breaking change v10, removes notice footers from descriptions
250
+ # return accumulator
251
+
240
252
  elif match := self.issue_selector.search(text):
241
253
  # if match := self.issue_selector.search(text):
242
254
  predicate = regexp(r",? and | *[,;/& ] *").sub(
@@ -252,11 +264,12 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
252
264
  predicate.split(","),
253
265
  )
254
266
  )
255
- accumulator["linked_issues"] = sort_numerically(
256
- set(accumulator["linked_issues"]).union(new_issue_refs)
257
- )
258
- # TODO: breaking change v10, removes resolution footers from descriptions
259
- # return accumulator
267
+ if new_issue_refs:
268
+ accumulator["linked_issues"] = sort_numerically(
269
+ set(accumulator["linked_issues"]).union(new_issue_refs)
270
+ )
271
+ # TODO: breaking change v10, removes resolution footers from descriptions
272
+ # return accumulator
260
273
 
261
274
  # Prevent appending duplicate descriptions
262
275
  if text not in accumulator["descriptions"]:
@@ -291,6 +304,7 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
291
304
  {
292
305
  "breaking_descriptions": [],
293
306
  "descriptions": [],
307
+ "notices": [],
294
308
  "linked_issues": [],
295
309
  },
296
310
  )
@@ -311,10 +325,15 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
311
325
  scope=parsed_scope,
312
326
  descriptions=tuple(body_components["descriptions"]),
313
327
  breaking_descriptions=tuple(body_components["breaking_descriptions"]),
328
+ release_notices=tuple(body_components["notices"]),
314
329
  linked_issues=tuple(body_components["linked_issues"]),
315
330
  linked_merge_request=linked_merge_request,
316
331
  )
317
332
 
333
+ @staticmethod
334
+ def is_merge_commit(commit: Commit) -> bool:
335
+ return len(commit.parents) > 1
336
+
318
337
  def parse_commit(self, commit: Commit) -> ParseResult:
319
338
  if not (parsed_msg_result := self.parse_message(force_str(commit.message))):
320
339
  return _logged_parse_error(
@@ -336,6 +355,11 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
336
355
  multiple commits, each of which will be parsed separately. Single commits
337
356
  will be returned as a list of a single ParseResult.
338
357
  """
358
+ if self.options.ignore_merge_commits and self.is_merge_commit(commit):
359
+ return _logged_parse_error(
360
+ commit, "Ignoring merge commit: %s" % commit.hexsha[:8]
361
+ )
362
+
339
363
  separate_commits: list[Commit] = (
340
364
  self.unsquash_commit(commit)
341
365
  if self.options.parse_squash_commits
@@ -442,7 +466,7 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
442
466
  [],
443
467
  )
444
468
 
445
- return separate_commit_msgs
469
+ return list(filter(None, separate_commit_msgs))
446
470
 
447
471
  def _find_squashed_commits_in_str(self, message: str) -> list[str]:
448
472
  separate_commit_msgs: list[str] = []
@@ -477,8 +501,4 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
477
501
 
478
502
  current_msg = clean_paragraph
479
503
 
480
- # Store the last commit message (if its not empty)
481
- if current_msg:
482
- separate_commit_msgs.append(current_msg)
483
-
484
- return separate_commit_msgs
504
+ return [*separate_commit_msgs, current_msg]