Skip to content

Commit 26c67a8

Browse files
authored
Merge pull request #171 from YunoHost/pathlib
2 parents 0bd4630 + f9b49aa commit 26c67a8

File tree

7 files changed

+202
-260
lines changed

7 files changed

+202
-260
lines changed

lib/lib_package_linter.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python3
22

3-
import os
3+
from pathlib import Path
44
import time
55
import urllib.request
66
from typing import Any, Callable, Generator, TypeVar
@@ -76,53 +76,53 @@ def urlopen(url: str) -> tuple[int, str]:
7676
return 200, conn.read().decode("UTF8")
7777

7878

79-
def file_exists(file_path: str) -> bool:
80-
return os.path.isfile(file_path) and os.stat(file_path).st_size > 0
79+
def not_empty(file: Path) -> bool:
80+
return file.is_file() and file.stat().st_size > 0
8181

8282

83-
def cache_file(cachefile: str, ttl_s: int) -> Callable[[Callable[..., str]], Callable[..., str]]:
83+
def cache_file(
84+
cachefile: Path, ttl_s: int
85+
) -> Callable[[Callable[..., str]], Callable[..., str]]:
8486
def cache_is_fresh() -> bool:
85-
return (
86-
os.path.exists(cachefile)
87-
and time.time() - os.path.getmtime(cachefile) < ttl_s
88-
)
87+
return cachefile.exists() and time.time() - cachefile.stat().st_mtime < ttl_s
8988

9089
def decorator(function: Callable[..., str]) -> Callable[..., str]:
9190
def wrapper(*args: Any, **kwargs: Any) -> str:
9291
if not cache_is_fresh():
93-
with open(cachefile, "w+") as outfile:
94-
outfile.write(function(*args, **kwargs))
95-
return open(cachefile).read()
92+
cachefile.write_text(function(*args, **kwargs))
93+
return cachefile.read_text()
9694

9795
return wrapper
9896

9997
return decorator
10098

10199

102-
@cache_file(".spdx_licenses", 3600)
100+
@cache_file(Path(".spdx_licenses"), 3600)
103101
def spdx_licenses() -> str:
104102
return urlopen("https://spdx.org/licenses/")[1]
105103

106104

107-
@cache_file(".manifest.v2.schema.json", 3600)
105+
@cache_file(Path(".manifest.v2.schema.json"), 3600)
108106
def manifest_v2_schema() -> str:
109107
url = "https://raw.githubusercontent.com/YunoHost/apps/master/schemas/manifest.v2.schema.json"
110108
return urlopen(url)[1]
111109

112110

113-
@cache_file(".tests.v1.schema.json", 3600)
111+
@cache_file(Path(".tests.v1.schema.json"), 3600)
114112
def tests_v1_schema() -> str:
115113
url = "https://raw.githubusercontent.com/YunoHost/apps/master/schemas/tests.v1.schema.json"
116114
return urlopen(url)[1]
117115

118116

119-
@cache_file(".config_panel.v1.schema.json", 3600)
117+
@cache_file(Path(".config_panel.v1.schema.json"), 3600)
120118
def config_panel_v1_schema() -> str:
121119
url = "https://raw.githubusercontent.com/YunoHost/apps/master/schemas/config_panel.v1.schema.json"
122120
return urlopen(url)[1]
123121

124122

125-
def validate_schema(name: str, schema: dict[str, Any], data: dict[str, Any]) -> Generator[Info, None, None]:
123+
def validate_schema(
124+
name: str, schema: dict[str, Any], data: dict[str, Any]
125+
) -> Generator[Info, None, None]:
126126
v = jsonschema.Draft7Validator(schema)
127127

128128
for error in v.iter_errors(data):
@@ -149,7 +149,6 @@ def validate_schema(name: str, schema: dict[str, Any], data: dict[str, Any]) ->
149149
}
150150

151151

152-
153152
def test(**kwargs: Any) -> Callable[[TestFn], TestFn]:
154153
def decorator(f: TestFn) -> TestFn:
155154
clsname = f.__qualname__.split(".")[0]
@@ -161,7 +160,7 @@ def decorator(f: TestFn) -> TestFn:
161160
return decorator
162161

163162

164-
class TestSuite():
163+
class TestSuite:
165164
name: str
166165
test_suite_name: str
167166

package_linter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def main():
3232
"""
3333
)
3434

35-
app = App(str(args.app_path))
35+
app = App(args.app_path)
3636
app.analyze()
3737

3838

tests/test_app.py

Lines changed: 44 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import copy
44
import json
55
import os
6+
from pathlib import Path
67
import subprocess
78
import sys
89
import tomllib
910
from typing import Generator
1011

1112
from lib.lib_package_linter import (Error, Info, Success, TestResult,
1213
TestSuite, Warning, config_panel_v1_schema,
13-
file_exists, test, tests_reports,
14+
not_empty, test, tests_reports,
1415
validate_schema)
1516
from lib.print import _print, is_json_output
1617
from tests.test_catalog import AppCatalog
@@ -228,9 +229,9 @@
228229

229230

230231
class App(TestSuite):
231-
def __init__(self, path: str) -> None:
232+
def __init__(self, path: Path) -> None:
232233

233-
_print(" Analyzing app %s ..." % path)
234+
_print(f" Analyzing app {path} ...")
234235
self.path = path
235236
self.manifest_ = Manifest(self.path)
236237
self.manifest = self.manifest_.manifest
@@ -358,18 +359,19 @@ def mandatory_scripts(app) -> TestResult:
358359
)
359360

360361
for filename in filenames:
361-
if not file_exists(app.path + "/" + filename):
362+
if not not_empty(app.path / filename):
362363
yield Error("Providing %s is mandatory" % filename)
363364

364-
if file_exists(app.path + "/LICENSE"):
365-
license_content = open(app.path + "/LICENSE").read()
365+
license = app.path / "LICENSE"
366+
if not_empty(license):
367+
license_content = license.read_text()
366368
if "File containing the license of your package" in license_content:
367369
yield Error("You should put an actual license in LICENSE...")
368370

369371
@test()
370372
def doc_dir(app) -> TestResult:
371373

372-
if not os.path.exists(app.path + "/doc"):
374+
if not (app.path / "doc").exists():
373375
yield Error(
374376
"""Having a doc/ folder is now mandatory in packaging v2 and is expected to contain :
375377
- (recommended) doc/DESCRIPTION.md : a long description of the app, typically around 5~20 lines, for example to list features
@@ -380,9 +382,9 @@ def doc_dir(app) -> TestResult:
380382
"""
381383
)
382384

383-
if os.path.exists(os.path.join(app.path, "doc/screenshots")):
385+
if (app.path / "doc" / "screenshots").exists():
384386
du_output = subprocess.check_output(
385-
["du", "-sb", app.path + "/doc/screenshots"], shell=False
387+
["du", "-sb", app.path / "doc" / "screenshots"], shell=False
386388
)
387389
screenshots_size = int(du_output.split()[0])
388390
if screenshots_size > 1024 * 1000:
@@ -394,25 +396,23 @@ def doc_dir(app) -> TestResult:
394396
"Please keep the content of doc/screenshots under ~512Kb. Having screenshots bigger than 512kb is probably a waste of resource and will take unecessarily long time to load on the webadmin UI and app catalog."
395397
)
396398

397-
for _, _, files in os.walk(os.path.join(app.path, "doc/screenshots")):
398-
for file in files:
399-
if file == ".gitkeep":
400-
continue
401-
if all(
402-
not file.lower().endswith(ext)
403-
for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp"]
404-
):
405-
yield Warning(
406-
"In the doc/screenshots folder, only .jpg, .jpeg, .png, .webp and .gif are accepted"
407-
)
408-
break
399+
for file in (app.path / "doc" / "screenshots").rglob("*"):
400+
filename = file.name
401+
if filename == ".gitkeep":
402+
continue
403+
if all(
404+
not filename.lower().endswith(ext)
405+
for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp"]
406+
):
407+
yield Warning(
408+
"In the doc/screenshots folder, only .jpg, .jpeg, .png, .webp and .gif are accepted"
409+
)
410+
break
409411

410412
@test()
411413
def doc_dir_v2(app) -> TestResult:
412414

413-
if os.path.exists(app.path + "/doc") and not os.path.exists(
414-
app.path + "/doc/DESCRIPTION.md"
415-
):
415+
if (app.path / "doc").exists() and not (app.path / "doc" / "DESCRIPTION.md").exists():
416416
yield Error(
417417
"A DESCRIPTION.md is now mandatory in packaging v2 and is meant to contains an extensive description of what the app is and does. Consider also adding a '/doc/screenshots/' folder with a few screenshots of what the app looks like."
418418
)
@@ -424,7 +424,7 @@ def doc_dir_v2(app) -> TestResult:
424424
):
425425
yield Error("It looks like DESCRIPTION.md just contains placeholder texts")
426426

427-
if os.path.exists(app.path + "/doc/DISCLAIMER.md"):
427+
if (app.path / "doc" / "DISCLAIMER.md").exists():
428428
yield Warning(
429429
"""DISCLAIMER.md has been replaced with several files in packaging v2 to improve the UX and provide the user with key information at the appropriate step of the app install / upgrade cycles.
430430
@@ -457,14 +457,14 @@ def admin_has_to_finish_install(app) -> TestResult:
457457
return
458458

459459
cmd = f"grep -q -IhEr '__DB_PWD__' '{app.path}/doc/'"
460-
if os.path.exists(app.path + "/doc") and os.system(cmd) == 0:
460+
if (app.path / "doc").exists() and os.system(cmd) == 0:
461461
yield Warning(
462462
"(doc folder) It looks like this app requires the admin to finish the install by entering DB credentials. Unless it's absolutely not easily automatizable, this should be handled automatically by the app install script using curl calls, or (CLI tools provided by the upstream maybe ?)."
463463
)
464464

465465
@test()
466466
def disclaimer_wording_or_placeholder(app) -> TestResult:
467-
if os.path.exists(app.path + "/doc"):
467+
if (app.path / "doc").exists():
468468
if (
469469
os.system(
470470
r"grep -nr -q 'Any known limitations, constrains or stuff not working, such as\|Other infos that people should be' %s/doc/"
@@ -505,47 +505,43 @@ def change_url_script(app) -> TestResult:
505505

506506
has_domain_arg = any(a["name"] == "domain" for a in args)
507507

508-
if has_domain_arg and not file_exists(app.path + "/scripts/change_url"):
508+
if has_domain_arg and not not_empty(app.path / "scripts" / "change_url"):
509509
yield Info(
510510
"Consider adding a change_url script to support changing where the app can be reached"
511511
)
512512

513513
@test()
514514
def config_panel(app) -> TestResult:
515515

516-
if file_exists(app.path + "/config_panel.json"):
516+
if not_empty(app.path / "config_panel.json"):
517517
yield Error(
518518
"JSON config panels are not supported anymore, should be replaced by a toml version"
519519
)
520520

521-
if file_exists(app.path + "/config_panel.toml.example"):
521+
if not_empty(app.path / "config_panel.toml.example"):
522522
yield Warning(
523523
"Please do not commit config_panel.toml.example ... This is just a 'documentation' for the config panel syntax meant to be kept in example_ynh"
524524
)
525525

526-
if not file_exists(app.path + "/config_panel.toml") and file_exists(
527-
app.path + "/scripts/config"
528-
):
526+
if not not_empty(app.path / "config_panel.toml") and not_empty(app.path / "scripts" / "config"):
529527
yield Warning(
530528
"The script 'config' exists but there is no config_panel.toml ... Please remove the 'config' script if this is just the example from example_ynh, or add a proper config_panel.toml if the point is really to have a config panel"
531529
)
532530

533-
if file_exists(app.path + "/config_panel.toml"):
534-
if (
535-
os.system(
536-
"grep -q 'version = \"0.1\"' '%s'"
537-
% (app.path + "/config_panel.toml")
538-
)
539-
== 0
540-
):
531+
if not_empty(app.path / "config_panel.toml"):
532+
check_old_panel = os.system(
533+
"grep -q 'version = \"0.1\"' '%s'"
534+
% (app.path / "config_panel.toml")
535+
)
536+
if check_old_panel == 0:
541537
yield Error(
542538
"Config panels version 0.1 are not supported anymore, should be adapted for version 1.0"
543539
)
544540
elif (
545-
os.path.exists(app.path + "/scripts/config")
541+
(app.path / "scripts" / "config").exists()
546542
and os.system(
547543
"grep -q 'YNH_CONFIG_\\|yunohost app action' '%s'"
548-
% (app.path + "/scripts/config")
544+
% (app.path / "scripts" / "config")
549545
)
550546
== 0
551547
):
@@ -556,22 +552,23 @@ def config_panel(app) -> TestResult:
556552
yield from validate_schema(
557553
"config_panel",
558554
json.loads(config_panel_v1_schema()),
559-
tomllib.load(open(app.path + "/config_panel.toml", "rb")),
555+
tomllib.load((app.path / "config_panel.toml").open("rb")),
560556
)
561557

562558
@test()
563559
def badges_in_readme(app) -> TestResult:
564560

565561
id_ = app.manifest["id"]
566562

567-
if not file_exists(app.path + "/README.md"):
563+
readme = app.path / "README.md"
564+
if not not_empty(readme):
568565
return
569566

570-
content = open(app.path + "/README.md").read()
567+
content = readme.read_text()
571568

572569
if (
573570
"This README was automatically generated" not in content
574-
or not "dash.yunohost.org/integration/%s.svg" % id_ in content
571+
or f"dash.yunohost.org/integration/{id_}.svg" not in content
575572
):
576573
yield Warning(
577574
"It looks like the README was not generated automatically by https://github.com/YunoHost/apps/tree/master/tools/README-generator. "

0 commit comments

Comments
 (0)