1
1
# -*- coding: utf-8 -*-
2
2
#
3
- # WinPython diff.py script
3
+ # WinPython diff.py script (streamlined, with historical and flexible modes)
4
4
# Copyright © 2013 Pierre Raybaut
5
5
# Copyright © 2014-2025+ The Winpython development team https://github.com/winpython/
6
6
# Licensed under the terms of the MIT License
7
- # (see winpython/__init__.py for details)
8
7
9
8
import os
10
- from pathlib import Path
11
9
import re
10
+ import sys
12
11
import shutil
12
+ from pathlib import Path
13
13
from packaging import version
14
- import sys
15
-
16
14
from . import utils
17
15
18
16
CHANGELOGS_DIR = Path (__file__ ).parent .parent / "changelogs"
@@ -22,12 +20,10 @@ class Package:
22
20
r"\[([\w\-\:\/\.\_]+)\]\(([^)]+)\) \| ([^\|]*) \| ([^\|]*)" , # SourceForge
23
21
r"\[([\w\-\:\/\.\_]+) ([^\]\ ]+)\] \| ([^\|]*) \| ([^\|]*)" # Google Code
24
22
]
25
-
26
23
def __init__ (self , text = None ):
27
24
self .name = self .url = self .version = self .description = None
28
25
if text :
29
26
self .from_text (text )
30
-
31
27
def from_text (self , text ):
32
28
for pattern in self .PATTERNS :
33
29
match = re .match (pattern , text )
@@ -36,31 +32,13 @@ def from_text(self, text):
36
32
return
37
33
raise ValueError (f"Unrecognized package line format: { text } " )
38
34
39
- def to_wiki (self ):
40
- return f" * [{ self .name } ]({ self .url } ) { self .version } ({ self .description } )\n "
41
-
42
- def upgrade_wiki (self , other ):
43
- return f" * [{ self .name } ]({ self .url } ) { other .version } → { self .version } ({ self .description } )\n "
44
-
45
35
class PackageIndex :
46
36
HEADERS = {"tools" : "### Tools" , "python" : "### Python packages" , "wheelhouse" : "### WheelHouse packages" }
47
37
BLANKS = ["Name | Version | Description" , "-----|---------|------------" , "" , "<details>" , "</details>" ]
48
38
49
- def __init__ (self , version , searchdir = None , flavor = "" , architecture = 64 ):
50
- self .version = version
51
- self .flavor = flavor
52
- self .searchdir = Path (searchdir ) if searchdir else CHANGELOGS_DIR
53
- self .architecture = architecture
54
- self .packages = {"tools" : {}, "python" : {}, "wheelhouse" : {}}
55
- self ._load_index ()
56
-
57
- def _load_index (self ):
58
- filename = self .searchdir / f"WinPython{ self .flavor } -{ self .architecture } bit-{ self .version } .md"
59
- if not filename .exists ():
60
- raise FileNotFoundError (f"Changelog not found: { filename } " )
61
-
62
- with open (filename , "r" , encoding = utils .guess_encoding (filename )[0 ]) as f :
63
- self ._parse_index (f .read ())
39
+ def __init__ (self , content ):
40
+ self .packages = {k : {} for k in self .HEADERS }
41
+ self ._parse_index (content )
64
42
65
43
def _parse_index (self , text ):
66
44
current = None
@@ -71,123 +49,163 @@ def _parse_index(self, text):
71
49
if line .strip () in self .BLANKS :
72
50
continue
73
51
if current :
74
- pkg = Package (line )
75
- self .packages [current ][pkg .name ] = pkg
52
+ try :
53
+ pkg = Package (line )
54
+ self .packages [current ][pkg .name ] = pkg
55
+ except Exception :
56
+ continue
76
57
77
58
def compare_packages (old , new ):
78
- """Return difference between package old and package new"""
79
-
80
- # wheel replace '-' per '_' in key
81
59
def normalize (d ): return {k .replace ("-" , "_" ).lower (): v for k , v in d .items ()}
82
60
old , new = normalize (old ), normalize (new )
61
+ added = [new [k ] for k in new if k not in old ]
62
+ upgraded = [new [k ] for k in new if k in old and new [k ].version != old [k ].version ]
63
+ removed = [old [k ] for k in old if k not in new ]
83
64
output = ""
84
-
85
- added = [new [k ].to_wiki () for k in new if k not in old ]
86
- upgraded = [new [k ].upgrade_wiki (old [k ]) for k in new if k in old and new [k ].version != old [k ].version ]
87
- removed = [old [k ].to_wiki () for k in old if k not in new ]
88
-
89
65
if added :
90
- output += "New packages:\n \n " + "" .join (added ) + " \n \n "
66
+ output += "\n New packages:\n " + "" .join (f" * { p . name } { p . version } ( { p . description } ) \n " for p in added )
91
67
if upgraded :
92
- output += "Upgraded packages:\n \n " + "" .join (upgraded ) + " \n \n "
68
+ output += "\n Upgraded packages:\n " + "" .join (f" * { p . name } { old [ p . name ]. version } → { p . version } ( { p . description } ) \n " for p in upgraded if p . name in old )
93
69
if removed :
94
- output += "Removed packages:\n \n " + "" .join (removed ) + "\n \n "
95
- return output
70
+ output += "\n Removed packages:\n " + "" .join (f" * { p .name } { p .version } ({ p .description } )\n " for p in removed )
71
+ return output or "\n No differences found.\n "
72
+
73
+ def compare_markdown_sections (md1 , md2 , header1 = "python" , header2 = "python" , label1 = "Input1" , label2 = "Input2" ):
74
+ pkgs1 = PackageIndex (md1 ).packages
75
+ pkgs2 = PackageIndex (md2 ).packages
76
+ diff = compare_packages (pkgs1 [header1 ], pkgs2 [header2 ])
77
+ # If comparing the same section, use the historical header
78
+ if header1 == header2 and header1 in PackageIndex .HEADERS :
79
+ title = PackageIndex .HEADERS [header1 ]
80
+ else :
81
+ title = f"## { label1 } [{ header1 } ] vs { label2 } [{ header2 } ]"
82
+ return f"{ title } \n \n { diff } "
83
+
84
+ def compare_markdown_section_pairs (md1 , md2 , header_pairs , label1 = "Input1" , label2 = "Input2" ):
85
+ pkgs1 = PackageIndex (md1 ).packages
86
+ pkgs2 = PackageIndex (md2 ).packages
87
+ text = f"# { label1 } vs { label2 } section-pairs comparison\n "
88
+ for h1 , h2 in header_pairs :
89
+ diff = compare_packages (pkgs1 [h1 ], pkgs2 [h2 ])
90
+ if diff .strip () and diff != "No differences found.\n " :
91
+ text += f"\n ## { label1 } [{ h1 } ] vs { label2 } [{ h2 } ]\n \n { diff } \n "
92
+ return text
93
+
94
+ def compare_files (file1 , file2 , mode = "full" , header1 = None , header2 = None , header_pairs = None ):
95
+ with open (file1 , encoding = utils .guess_encoding (file1 )[0 ]) as f1 , \
96
+ open (file2 , encoding = utils .guess_encoding (file2 )[0 ]) as f2 :
97
+ md1 , md2 = f1 .read (), f2 .read ()
98
+ if mode == "full" :
99
+ result = ""
100
+ for k in PackageIndex .HEADERS :
101
+ result += compare_markdown_sections (md1 , md2 , k , k , file1 , file2 ) + "\n "
102
+ return result
103
+ elif mode == "section" :
104
+ return compare_markdown_sections (md1 , md2 , header1 , header2 , file1 , file2 )
105
+ elif mode == "pairs" :
106
+ return compare_markdown_section_pairs (md1 , md2 , header_pairs , file1 , file2 )
107
+ else :
108
+ raise ValueError ("Unknown mode." )
109
+
110
+ # --- ORIGINAL/HISTORICAL VERSION-TO-VERSION COMPARISON ---
96
111
97
112
def find_previous_version (target_version , searchdir = None , flavor = "" , architecture = 64 ):
98
- """Find version which is the closest to `version`"""
99
113
search_dir = Path (searchdir ) if searchdir else CHANGELOGS_DIR
100
114
pattern = re .compile (rf"WinPython{ flavor } -{ architecture } bit-([0-9\.]+)\.(txt|md)" )
101
115
versions = [pattern .match (f ).group (1 ) for f in os .listdir (search_dir ) if pattern .match (f )]
102
116
versions = [v for v in versions if version .parse (v ) < version .parse (target_version )]
103
117
return max (versions , key = version .parse , default = target_version )
104
118
119
+ def load_version_markdown (version , searchdir , flavor = "" , architecture = 64 ):
120
+ filename = Path (searchdir ) / f"WinPython{ flavor } -{ architecture } bit-{ version } .md"
121
+ if not filename .exists ():
122
+ raise FileNotFoundError (f"Changelog not found: { filename } " )
123
+ with open (filename , "r" , encoding = utils .guess_encoding (filename )[0 ]) as f :
124
+ return f .read ()
125
+
105
126
def compare_package_indexes (version2 , version1 = None , searchdir = None , flavor = "" , flavor1 = None , architecture = 64 ):
106
- """Comparison by looking versions in a given Changelog directory"""
107
- if not searchdir or (not Path (searchdir ).is_dir () and not CHANGELOGS_DIR .is_dir ()):
108
- print (f"Error: changelogs directory { CHANGELOGS_DIR } does not exist." )
109
- sys .exit (1 )
127
+ searchdir = Path (searchdir ) if searchdir else CHANGELOGS_DIR
110
128
version1 = version1 or find_previous_version (version2 , searchdir , flavor , architecture )
111
129
flavor1 = flavor1 or flavor
112
-
113
- pi1 = PackageIndex (version1 , searchdir , flavor1 , architecture )
114
- pi2 = PackageIndex (version2 , searchdir , flavor , architecture )
115
-
116
- text = (
130
+ md1 = load_version_markdown (version1 , searchdir , flavor1 , architecture )
131
+ md2 = load_version_markdown (version2 , searchdir , flavor , architecture )
132
+ result = f"# WinPython { architecture } bit { version2 } { flavor } vs { version1 } { flavor1 } \n "
133
+ result = (
117
134
f"## History of changes for WinPython-{ architecture } bit { version2 + flavor } \r \n \r \n "
118
135
f"The following changes were made to WinPython-{ architecture } bit distribution since version { version1 + flavor1 } .\n \n \n "
119
136
"<details>\n \n "
120
137
)
121
-
122
- for key in PackageIndex .HEADERS :
123
- diff = compare_packages (pi1 .packages [key ], pi2 .packages [key ])
124
- if diff :
125
- text += f"\n { PackageIndex .HEADERS [key ]} \n \n { diff } "
126
-
127
- return text + "\n </details>\n \n * * *\n "
138
+ for k in PackageIndex .HEADERS :
139
+ result += compare_markdown_sections (md1 , md2 , k , k , version1 , version2 ) + "\n "
140
+ return result + "\n </details>\n \n * * *\n "
128
141
129
142
def copy_changelogs (version , searchdir , flavor = "" , architecture = 64 , basedir = None ):
130
- basever = "." .join (version .split ("." )[:2 ])
143
+ """Copy all changelogs for a major.minor version into basedir."""
144
+ basever = "." .join (str (version ).split ("." )[:2 ])
131
145
pattern = re .compile (rf"WinPython{ flavor } -{ architecture } bit-{ basever } [0-9\.]*\.(txt|md)" )
132
146
dest = Path (basedir )
133
147
for fname in os .listdir (searchdir ):
134
148
if pattern .match (fname ):
135
- shutil .copyfile (searchdir / fname , dest / fname )
149
+ shutil .copyfile (Path ( searchdir ) / fname , dest / fname )
136
150
137
151
def write_changelog (version2 , version1 = None , searchdir = None , flavor = "" , architecture = 64 , basedir = None ):
138
- """Write changelog between version1 and version2 of WinPython"""
152
+ """Write changelog between version1 and version2 of WinPython."""
153
+ searchdir = Path (searchdir ) if searchdir else CHANGELOGS_DIR
139
154
if basedir :
140
155
copy_changelogs (version2 , searchdir , flavor , architecture , basedir )
141
- print ("comparing_package_indexes" , version2 , searchdir , flavor , architecture )
142
156
changelog = compare_package_indexes (version2 , version1 , searchdir , flavor , architecture = architecture )
143
157
output_file = searchdir / f"WinPython{ flavor } -{ architecture } bit-{ version2 } _History.md"
144
158
with open (output_file , "w" , encoding = "utf-8" ) as f :
145
159
f .write (changelog )
146
- # Copy to winpython/changelogs back to basedir
147
160
if basedir :
148
161
shutil .copyfile (output_file , Path (basedir ) / output_file .name )
149
162
150
- def compare_two_markdown_files (file1 , file2 ):
151
- """Compare two arbitrary markdown files with WinPython changelog format."""
152
- class DummyPackageIndex (PackageIndex ):
153
- def __init__ (self , filename ):
154
- self .packages = {"tools" : {}, "python" : {}, "wheelhouse" : {}}
155
- self ._load_index (filename )
156
-
157
- def _load_index (self , filename ):
158
- with open (filename , "r" , encoding = utils .guess_encoding (filename )[0 ]) as f :
159
- self ._parse_index (f .read ())
160
-
161
- pi1 = DummyPackageIndex (Path (file1 ))
162
- pi2 = DummyPackageIndex (Path (file2 ))
163
-
164
- text = f"## Differences between { file1 } and { file2 } \n \n <details>\n \n "
165
- for key in PackageIndex .HEADERS :
166
- diff = compare_packages (pi1 .packages [key ], pi2 .packages [key ])
167
- if diff :
168
- text += f"\n { PackageIndex .HEADERS [key ]} \n \n { diff } "
169
- return text + "\n </details>\n \n * * *\n "
170
-
171
163
def print_usage ():
172
164
print ("Usage:" )
173
165
print (" python diff.py file1.md file2.md" )
174
- print (" - Compare two markdown changelog files directly." )
166
+ print (" - Compare all sections of two markdown files." )
167
+ print (" python diff.py file1.md file2.md --section header1 header2" )
168
+ print (" - Compare section 'header1' of file1 with section 'header2' of file2." )
169
+ print (" python diff.py file1.md file2.md --pairs header1a header2a [header1b header2b ...]" )
170
+ print (" - Compare pairs of sections. Example: python diff.py f1.md f2.md --pairs python wheelhouse tools tools" )
175
171
print (" python diff.py <version2> <version1> [searchdir] [flavor] [architecture]" )
176
- print (" - Compare WinPython markdown changelogs by version." )
172
+ print (" - Compare WinPython markdown changelogs by version (historical mode)." )
173
+ print (" python diff.py --write-changelog <version2> <version1> [searchdir] [flavor] [architecture] [basedir]" )
174
+ print (" - Write changelog between version1 and version2 to file (and optionally copy to basedir)." )
177
175
178
176
if __name__ == "__main__" :
179
- if len (sys .argv ) == 3 and all (arg .lower ().endswith ('.md' ) for arg in sys .argv [1 :]):
180
- # Usage: python diff.py file1.md file2.md
181
- file1 , file2 = sys .argv [1 ], sys .argv [2 ]
182
- print (compare_two_markdown_files (file1 , file2 ))
183
- elif len (sys .argv ) >= 3 :
184
- # Original usage (version comparison)
185
- # Example: python diff.py 3.7.4.0 3.7.2.0 "C:\WinP\bd37\budot" "Zero" 32
186
- version2 = sys .argv [1 ]
187
- version1 = sys .argv [2 ]
188
- searchdir = Path (sys .argv [3 ]) if len (sys .argv ) > 3 else CHANGELOGS_DIR
189
- flavor = sys .argv [4 ] if len (sys .argv ) > 4 else ""
190
- architecture = int (sys .argv [5 ]) if len (sys .argv ) > 5 else 64
177
+ args = sys .argv
178
+ if len (args ) >= 3 and all (arg .lower ().endswith ('.md' ) for arg in args [1 :3 ]):
179
+ file1 , file2 = args [1 ], args [2 ]
180
+ if len (args ) == 3 :
181
+ print (compare_files (file1 , file2 ))
182
+ elif args [3 ] == "--section" and len (args ) >= 6 :
183
+ h1 , h2 = args [4 ], args [5 ]
184
+ print (compare_files (file1 , file2 , mode = "section" , header1 = h1 , header2 = h2 ))
185
+ elif args [3 ] == "--pairs" and len (args ) > 4 and len (args [4 :]) % 2 == 0 :
186
+ pairs = list (zip (args [4 ::2 ], args [5 ::2 ]))
187
+ print (compare_files (file1 , file2 , mode = "pairs" , header_pairs = pairs ))
188
+ else :
189
+ print_usage ()
190
+ elif len (args ) >= 2 and args [1 ] == "--write-changelog" :
191
+ # Usage: --write-changelog <version2> <version1> [searchdir] [flavor] [architecture] [basedir]
192
+ if len (args ) < 4 :
193
+ print_usage ()
194
+ sys .exit (1 )
195
+ version2 = args [2 ]
196
+ version1 = args [3 ]
197
+ searchdir = args [4 ] if len (args ) > 4 else CHANGELOGS_DIR
198
+ flavor = args [5 ] if len (args ) > 5 else ""
199
+ architecture = int (args [6 ]) if len (args ) > 6 else 64
200
+ basedir = args [7 ] if len (args ) > 7 else None
201
+ write_changelog (version2 , version1 , searchdir , flavor , architecture , basedir )
202
+ print (f"Changelog written for { version2 } vs { version1 } ." )
203
+ elif len (args ) >= 3 :
204
+ version2 = args [1 ]
205
+ version1 = args [2 ] if len (args ) > 2 and not args [2 ].endswith ('.md' ) else None
206
+ searchdir = args [3 ] if len (args ) > 3 else CHANGELOGS_DIR
207
+ flavor = args [4 ] if len (args ) > 4 else ""
208
+ architecture = int (args [5 ]) if len (args ) > 5 else 64
191
209
print (compare_package_indexes (version2 , version1 , searchdir , flavor , architecture = architecture ))
192
210
else :
193
211
print_usage ()
0 commit comments