77import click
88import colorama
99import frontmatter
10- import pkg_resources
10+ from packaging . specifiers import SpecifierSet
1111from voluptuous import All , Any , Invalid , Length , Optional , Required , Schema , Url
1212
1313OCTOPRINT_PY3_SUPPORT = ("3.7" , "3.8" , "3.9" , "3.10" , "3.11" , "3.12" )
1414
1515NonEmptyString = All (str , Length (min = 1 ))
1616
17+ class ValidationError (ValueError ):
18+ def __init__ (self , * args , error_messages = None ):
19+ super ().__init__ (* args )
20+ self .error_messages = error_messages
21+
1722
1823def to_requirement (compat , name = "Foo" ):
1924 if not any (
2025 compat .startswith (c ) for c in ("<" , "<=" , "!=" , "==" , ">=" , ">" , "~=" , "===" )
2126 ):
2227 compat = ">={}" .format (compat )
23- return pkg_resources . Requirement . parse ( name + compat )
28+ return SpecifierSet ( compat )
2429
2530
2631def Version (v ):
@@ -88,7 +93,7 @@ def ImageLocation(v):
8893 Optional ("disabled" ): NonEmptyString ,
8994 Optional ("abandoned" ): NonEmptyString ,
9095 Optional ("up_for_adoption" ): Url (),
91- Optional ("redirect_from" ): NonEmptyString ,
96+ Optional ("redirect_from" ): Any ( All ([ NonEmptyString ]), NonEmptyString ) ,
9297 Optional ("attributes" ): Any (list , None ),
9398 }
9499)
@@ -128,7 +133,7 @@ def check_url(url):
128133 # image url is a path
129134 image_path = os .path .abspath (os .path .join (src , url [1 :]))
130135 if not os .path .exists (image_path ):
131- raise Invalid (
136+ raise ValidationError (
132137 "image location '{}' doesn't exist on disk ({})" .format (
133138 url , image_path
134139 )
@@ -144,7 +149,7 @@ def check_url(url):
144149 return []
145150
146151
147- def validate_image_urls (data , path ):
152+ def validate_internal_assets (data , path ):
148153 warnings = []
149154
150155 filename = os .path .basename (path )[:- 3 ]
@@ -183,24 +188,50 @@ def validate_id_match(data, path):
183188 filename = os .path .basename (path )[:- 3 ]
184189 if data ["id" ] != filename :
185190 return [
186- "id '{}' does not match file name '{}.md' @ data['id']" .format (
191+ "id '{}' does not match file name '{}.md', please rename the file @ data['id']" .format (
187192 data ["id" ], filename
188193 )
189194 ]
190195 return []
191196
192197
198+ def validate_asset_match (data , path ):
199+ errors = []
200+
201+ filename = os .path .basename (path )[:- 3 ]
202+
203+ def check_url (loc , url ):
204+ if url .startswith ("/assets/img/plugins/" ) and not url .startswith ("/assets/img/plugins/{}" .format (filename )):
205+ folder = url .split ("/" )[4 ]
206+ message = "asset folder of image '{}' does not match plugin identifier {}: {} @ {}" .format (url , filename , folder , loc )
207+ errors .append (message )
208+
209+ if "screenshots" in data :
210+ count = 0
211+ for entry in data ["screenshots" ]:
212+ check_url ("data['screenshots'][{}]['url']" .format (count ), entry ["url" ])
213+ count += 1
214+
215+ if "featuredimage" in data :
216+ check_url ("data['featuredimage']" , data ["featuredimage" ])
217+
218+ if errors :
219+ raise ValidationError ("plugin references assets in mismatched asset folder" , error_messages = errors )
220+
221+ return []
222+
223+
193224def validate_python_compatibility (data ):
194225 warnings = []
195226
196227 if "compatibility" in data and "python" in data ["compatibility" ]:
197228 requirement = to_requirement (data ["compatibility" ]["python" ], name = "Python" )
198229 if all (map (lambda x : x not in requirement , OCTOPRINT_PY3_SUPPORT )):
199- raise ValueError (
230+ raise ValidationError (
200231 "python compatibility does not include Python 3 @ data['compatibility']['python']"
201232 )
202233 else :
203- raise ValueError ("not flagged as Python 3 compatible @ data" )
234+ raise ValidationError ("not flagged as Python 3 compatible @ data" )
204235 return warnings
205236
206237
@@ -215,13 +246,13 @@ def validate_date_unchanged(data, path, src, sha, debug=False):
215246 try :
216247 output = subprocess .check_output (command , encoding = "utf-8" )
217248 if not output :
218- raise ValueError ("could not read prior version" )
249+ raise ValidationError ("could not read prior version" )
219250 except subprocess .CalledProcessError :
220251 return
221252
222253 old_metadata , old_content = frontmatter .parse (output )
223254 if data ["date" ] != old_metadata .get ("date" ):
224- raise ValueError (
255+ raise ValidationError (
225256 "date must not be changed after initial registration @ data['date']"
226257 )
227258
@@ -232,6 +263,7 @@ def validate(
232263 src ,
233264 path ,
234265 id_match = False ,
266+ asset_match = False ,
235267 internal_assets = False ,
236268 date_unchanged = False ,
237269 screenshots_present = False ,
@@ -250,8 +282,11 @@ def validate(
250282 if id_match :
251283 warnings += validate_id_match (metadata , path )
252284
285+ if asset_match :
286+ warnings += validate_asset_match (metadata , path )
287+
253288 if internal_assets :
254- warnings += validate_image_urls (metadata , path )
289+ warnings += validate_internal_assets (metadata , path )
255290
256291 if screenshots_present :
257292 warnings += validate_screenshots_present (metadata )
@@ -274,6 +309,7 @@ def validate(
274309@click .option ("--debug" , is_flag = True )
275310@click .option ("--src" , "src" )
276311@click .option ("--check-id-match" , "id_match" , is_flag = True )
312+ @click .option ("--check-asset-match" , "asset_match" , is_flag = True )
277313@click .option ("--check-internal-assets" , "internal_assets" , is_flag = True )
278314@click .option (
279315 "--check-date-unchanged" ,
@@ -289,6 +325,7 @@ def main(
289325 debug = False ,
290326 src = None ,
291327 id_match = False ,
328+ asset_match = False ,
292329 internal_assets = False ,
293330 date_unchanged = None ,
294331 screenshots_present = False ,
@@ -321,6 +358,7 @@ def main(
321358 src ,
322359 path ,
323360 id_match = id_match ,
361+ asset_match = asset_match ,
324362 internal_assets = internal_assets ,
325363 date_unchanged = date_unchanged ,
326364 screenshots_present = screenshots_present ,
@@ -331,6 +369,13 @@ def main(
331369 print ("{}: " .format (path ), end = "" )
332370 print (colorama .Fore .RED + colorama .Style .BRIGHT + "FAIL" )
333371
372+ if isinstance (exc , ValidationError ) and exc .error_messages :
373+ for error in exc .error_messages :
374+ if action_output :
375+ print ("::error file={}::{}" .format (path [len (src ) + 1 :], error ))
376+ else :
377+ print (" " + error )
378+
334379 if action_output :
335380 print ("::error file={}::{}" .format (path [len (src ) + 1 :], str (exc )))
336381 else :
0 commit comments