-
-
Notifications
You must be signed in to change notification settings - Fork 85
Create fd.o spec audit script #1340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
wpkelso
wants to merge
33
commits into
elementary:main
Choose a base branch
from
wpkelso:audit-script
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
edb6a2d
add script and spec file
wpkelso e1961f1
Output list of files in csv file
wpkelso 582d542
Add help usage message
wpkelso 6968c10
Merge branch 'main' into audit-script
wpkelso 679b88e
correct noted entries
wpkelso aef1a29
Exclude symbolic icons from report
wpkelso d016dad
Symbolic is common practice enough that showing it in out-of-spec isn…
wpkelso 75c1a09
Merge branch 'main' into audit-script
wpkelso b7a7f57
add optional support for pulling the spec off the web
wpkelso ac84436
Clean up remote pull and use argparse like a civilized script
wpkelso 94ffd92
Merge branch 'main' into audit-script
wpkelso 48e0075
Merge branch 'main' into audit-script
wpkelso e2d3e8f
Merge branch 'main' into audit-script
wpkelso 18aa636
Merge branch 'main' into audit-script
wpkelso 7f7fbb3
Merge branch 'main' into audit-script
zeebok 12b0688
Merge branch 'main' into audit-script
danirabbit da94c8e
document script and add .venv to gitignore
wpkelso 5ab6511
mention python3 system package reqs
wpkelso 317f3e2
set a user agent for requests
wpkelso 13df2f5
rm debug print
wpkelso e17736b
pythonify and restrict considered files to svg
wpkelso ebab803
specf called incorrectly
wpkelso 76b90cf
correct typos
wpkelso eb31477
add option to specify which list to report
wpkelso faa73e2
remove flag icon code
wpkelso 74fc4cf
Generalize and don't pull from web if file is specified
wpkelso 5e78392
use more standard convetion for output file name
wpkelso 7251c08
flag-gate file output
wpkelso 7b1b006
Flag-gate symbolics and add color to missing entries
wpkelso 2fa1485
Add note on limitations with build system symlinks
wpkelso d3ae6af
symbolics aren't part of the spec at all, they're gtk platform specific
wpkelso 0c0b742
Merge branch 'main' into audit-script
wpkelso ba05e3c
Merge branch 'main' into audit-script
wpkelso File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| *~ | ||
| build/ | ||
| .venv/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,275 @@ | ||
| #!/usr/bin/env python3 | ||
|
|
||
| # # Fd.o Icon Name Specification Audit | ||
|
|
||
| # ## Description: | ||
| # | ||
| # Audit a Freedesktop.org icon specification-compatible theme against the icon | ||
| # naming specification, returning a report of what icons were included that are | ||
| # in-spec, what icons were included that are out-of-spec, and what icons were | ||
| # missing that are included in the spec. | ||
|
|
||
| # ## Dependencies: | ||
| # | ||
| # - requests (optional) | ||
| # - bs4 (optional) | ||
|
|
||
| import os, csv, argparse | ||
|
|
||
| parser = argparse.ArgumentParser( | ||
| prog = "audit", | ||
| description = "Audit a FreeDesktop.org compatible icon set against a specification" | ||
| ) | ||
| parser.add_argument( | ||
| "theme_path", | ||
| ) | ||
| parser.add_argument( | ||
| "-s", | ||
| "--specification", | ||
| dest="specf" | ||
| ) | ||
| parser.add_argument( | ||
| "-v", | ||
| "--verbose", | ||
| action="store_true" | ||
| ) | ||
| parser.add_argument( | ||
| "-o", | ||
| "--output", | ||
| dest="fout", | ||
| ) | ||
| parser.add_argument( | ||
| "-d", | ||
| "--display-results", | ||
| dest="to_display", | ||
| choices=["all", "found", "missing", "extra"], | ||
| default="all" | ||
| ) | ||
| args = parser.parse_args() | ||
|
|
||
| # make hrules pretty | ||
| try: | ||
| view_width = os.get_terminal_size().columns | ||
| except: | ||
| view_width = 80 | ||
|
|
||
| # Check for optional dependencies to enable drawing the spec from the web. | ||
| # Otherwise fall back to included file | ||
| try: | ||
| found_deps = False | ||
| import requests | ||
| from bs4 import BeautifulSoup | ||
| except: | ||
| print("Couldn't find bs4 and requests dependencies, falling back to using spec file…") | ||
| else: | ||
| if args.specf is None: | ||
| print("Found bs4 and requests dependencies, pulling spec from the web…") | ||
| found_deps = True | ||
|
|
||
| def get_soup(url: str) -> BeautifulSoup: | ||
| page = session.get(url) | ||
| soup = BeautifulSoup(page.text, "html.parser") | ||
| return soup | ||
|
|
||
| # This is kinda not great but it works | ||
| def parse_soup(soup: BeautifulSoup) -> list: | ||
| result = [] | ||
|
|
||
| tables = soup.find_all("table") | ||
| #remove the context description table, we don't care about it here | ||
| tables.pop(0) | ||
| for table in tables: | ||
| rows = table.find_all("tr") | ||
| for row in rows: | ||
| text = row.contents | ||
| name = text[0].text.strip().replace("\n", "") | ||
| # skip the header row | ||
| if name == "Name": continue | ||
| result.append(name) | ||
|
|
||
| return result | ||
|
|
||
| def pad_list(iterable: list, length: int) -> list: | ||
| extension_length = max(length - len(iterable), 0) | ||
| if args.verbose: print(f"Extending list by {extension_length} elements") | ||
| for _ in range(extension_length): | ||
| iterable.append(None) | ||
|
|
||
| return iterable | ||
|
|
||
| # Place this in a top level context so we don't have to pass it around | ||
| ignore_list = [] | ||
|
|
||
| def populate_from_dir(dir: os.DirEntry) -> set: | ||
| # This function is called recursively to populate a list | ||
| tree = [] | ||
|
|
||
| for entry in os.scandir(dir.path): | ||
| if entry.name in ignore_list: | ||
| continue | ||
|
|
||
| if entry.is_dir(): | ||
| if args.verbose: print(f"Found dir at {entry.path}, going deeper") | ||
| # A spoonful of recursion to help the medicine go down | ||
| tree.extend(populate_from_dir(entry)) | ||
| elif entry.name.endswith(".svg"): | ||
| # We only check svg because that's the only type we use in elementary icons | ||
| if args.verbose: print(f"Adding file {entry.path}, named {entry.name} to SubTree") | ||
| tree.append(entry.name.removesuffix(".svg")) | ||
|
|
||
| if args.verbose: print(f"The SubTree for Dir {dir.name} is {tree}") | ||
| return list(set(tree)) | ||
|
|
||
| # Load a text stream of the spec into a dictionary, where KEY=icon_name & | ||
| # VALUE=a bool denoting whether the file was found in the file tree | ||
| # Iterate over the list, checking to see if the specified file can be found. | ||
| specification = {} | ||
|
|
||
| if found_deps: | ||
| url = "https://specifications.freedesktop.org/icon-naming-spec/latest/" | ||
| headers = { | ||
| 'User-Agent': 'fdo-icon-name-spec-audit-script/1.0.0', | ||
| } | ||
|
|
||
| session = requests.Session() | ||
| session.headers.update(headers) | ||
|
|
||
| soup = get_soup(url) | ||
| spec_list = parse_soup(soup) | ||
| specification = specification.fromkeys(spec_list, False) | ||
| else: | ||
| with open(args.specf, "r") as file: | ||
| print(f"Loading specification in file: {args.specf}…") | ||
| for line in file: | ||
| if line.startswith(("//", " ", "\n")): | ||
| continue | ||
| specification.setdefault(line.removesuffix("\n"), False) | ||
|
|
||
| print("Successfully loaded specification!") | ||
|
|
||
| # Change the working dir to our target dir, to make it easier to traverse the | ||
| # tree. Try to find the file `.auditignore` in the dir root and load it into | ||
| # a list. Anything in this list doesn't exist as far as this script is | ||
| # concerned. | ||
| try: | ||
| target_dir = args.theme_path | ||
| except: | ||
| print("Please specify a directory") | ||
| exit() | ||
| else: | ||
| # Save the cwd so we can switch back later | ||
| script_dir = os.getcwd() | ||
| dir = os.fspath(target_dir) | ||
| os.chdir(dir) | ||
|
|
||
| try: | ||
| with open(".auditignore", "r") as file: | ||
|
wpkelso marked this conversation as resolved.
|
||
| print("Found .auditignore file!") | ||
| for line in file: | ||
| ignore_list.append(line.strip()) | ||
| except: | ||
| print("No .auditignore file found in root") | ||
| else: | ||
| print("Loaded .auditignore file!") | ||
| if args.verbose: print(f"Values contained: {ignore_list}") | ||
|
|
||
| print('-' * view_width) | ||
|
|
||
| # Generate the contents of the relevant directory tree | ||
| contents = [] | ||
|
|
||
| for entry in os.scandir(): | ||
| if entry.name not in ignore_list: | ||
| if entry.is_dir(): | ||
| if args.verbose: print(f"Found dir at {entry.path}") | ||
| sub_dir_tree = populate_from_dir(entry) | ||
| contents.extend(sub_dir_tree) | ||
| else: | ||
| if entry.name.endswith(".svg") and not | ||
| entry.name.endswith("-symbolic.svg"): | ||
| if args.verbose: print(f"Adding file {entry.path} to tree") | ||
| contents.append(entry.name.removesuffix(".svg")) | ||
|
|
||
| # remove duplicate names from the list | ||
| contents = list(set(contents)) | ||
|
|
||
| if args.verbose: | ||
| print(f"The full dirTree is {contents}") | ||
| print('-' * view_width) | ||
|
|
||
| # Traverse the dir tree, checking whether the found file is in the spec list. | ||
| # This is so we don't have to traverse the entire dir tree of the set, just the | ||
| # specification list which is likely to be much smaller. | ||
|
|
||
| for entry in specification.keys(): | ||
| if entry in contents: | ||
| print(f"Found {entry}") | ||
| specification |= {entry: True} | ||
| else: | ||
| print(f"\033[31m[!!] {entry} is missing!\033[0m") | ||
|
|
||
|
|
||
| # Calculate percent spec coverage | ||
| total_entries = len(specification.keys()) | ||
| existant_color_entries = list(specification.values()).count(True) | ||
| color_results = list(specification.values()) | ||
|
|
||
| print(f"{existant_color_entries / total_entries * 100:.2f}% coverage of specification") | ||
|
|
||
| print('-' * view_width) | ||
|
|
||
| if args.fout is not None: | ||
| os.chdir(script_dir) | ||
|
|
||
| # Write report file with Found, Missing, and Extra information | ||
| spec_list = list(specification.keys()) | ||
| found_entries = [] | ||
| missing_entries = [] | ||
| extra_entries = [] | ||
|
|
||
| # We only look at the color entry results, as symbolic is not officially in the spec | ||
| for i, result in enumerate(color_results): | ||
| # If the result is true, then it was found | ||
| # If the result is false, then it is missing | ||
| name = spec_list[i] | ||
| if result: | ||
| if args.verbose: print(f"Adding {name} to Found…") | ||
| found_entries.append(name) | ||
| else: | ||
| if args.verbose: print(f"Adding {name} to Missing…") | ||
| missing_entries.append(name) | ||
|
|
||
| # Now we want to go through all of the entries we found in the initial contents and | ||
| # point out those that aren't in the spec | ||
| for entry in contents: | ||
| if entry not in spec_list and not entry.endswith("-symbolic"): | ||
| if args.verbose: print(f"Adding {entry} to Extra") | ||
| extra_entries.append(entry) | ||
|
|
||
| # Pad all lists to be the same length, then zip all three lists together | ||
| length = max(len(found_entries), len(missing_entries), len(extra_entries)) | ||
|
|
||
| with open(args.fout, 'w', newline='') as file: | ||
| fwriter = csv.writer(file) | ||
| match args.to_display: | ||
| case "found": | ||
| fwriter.writerow(["Found"]) | ||
| fwriter.writerows(map(lambda x: [x], found_entries)) | ||
| case "missing": | ||
| fwriter.writerow(["Missing"]) | ||
| fwriter.writerows(map(lambda x: [x], missing_entries)) | ||
| case "extra": | ||
| fwriter.writerow(["Extra"]) | ||
| fwriter.writerows(map(lambda x: [x], extra_entries)) | ||
| case _: | ||
| fwriter.writerow(["Found", "Missing", "Extra"]) | ||
|
|
||
| found_entries = pad_list(found_entries, length) | ||
| missing_entries = pad_list(missing_entries, length) | ||
| extra_entries = pad_list(extra_entries, length) | ||
| zipped_lists = [(a, b, c) for a, b, c in zip(found_entries, | ||
| missing_entries, extra_entries)] | ||
| fwriter.writerows(zipped_lists) | ||
|
|
||
| print(f"Report written to {os.getcwd()}/{args.fout}") | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.