nixos-render-docs-redirects: init (#357383)
Co-authored-by: Valentin Gagarin <valentin@gagarin.work>
This commit is contained in:
parent
c8089754ee
commit
836d207c6c
@ -9,6 +9,8 @@
|
||||
mkShellNoCC,
|
||||
documentation-highlighter,
|
||||
nixos-render-docs,
|
||||
nixos-render-docs-redirects,
|
||||
writeShellScriptBin,
|
||||
nixpkgs ? { },
|
||||
}:
|
||||
|
||||
@ -105,8 +107,14 @@ stdenvNoCC.mkDerivation (
|
||||
buildArgs = "./.";
|
||||
open = "/share/doc/nixpkgs/manual.html";
|
||||
};
|
||||
nixos-render-docs-redirects' = writeShellScriptBin "redirects" "${lib.getExe nixos-render-docs-redirects} --file ${toString ../redirects.json} $@";
|
||||
in
|
||||
mkShellNoCC { packages = [ devmode' ]; };
|
||||
mkShellNoCC {
|
||||
packages = [
|
||||
devmode'
|
||||
nixos-render-docs-redirects'
|
||||
];
|
||||
};
|
||||
|
||||
tests.manpage-urls = callPackage ../tests/manpage-urls.nix { };
|
||||
};
|
||||
|
@ -10,7 +10,11 @@ let
|
||||
buildArgs = "../../release.nix -A manualHTML.${builtins.currentSystem}";
|
||||
open = "/${outputPath}/${indexPath}";
|
||||
};
|
||||
nixos-render-docs-redirects = pkgs.writeShellScriptBin "redirects" "${pkgs.lib.getExe pkgs.nixos-render-docs-redirects} --file ${toString ./redirects.json} $@";
|
||||
in
|
||||
pkgs.mkShellNoCC {
|
||||
packages = [ devmode ];
|
||||
packages = [
|
||||
devmode
|
||||
nixos-render-docs-redirects
|
||||
];
|
||||
}
|
||||
|
22
pkgs/by-name/ni/nixos-render-docs-redirects/package.nix
Normal file
22
pkgs/by-name/ni/nixos-render-docs-redirects/package.nix
Normal file
@ -0,0 +1,22 @@
|
||||
{ lib, python3 }:
|
||||
|
||||
python3.pkgs.buildPythonApplication {
|
||||
pname = "nixos-render-docs-redirects";
|
||||
version = "0.0";
|
||||
pyproject = true;
|
||||
|
||||
src = ./src;
|
||||
|
||||
build-system = with python3.pkgs; [ setuptools ];
|
||||
|
||||
nativeCheckInputs = with python3.pkgs; [
|
||||
pytestCheckHook
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "Redirects manipulation for nixos manuals";
|
||||
license = lib.licenses.mit;
|
||||
maintainers = with lib.maintainers; [ getpsyched ];
|
||||
mainProgram = "redirects";
|
||||
};
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def add_content(redirects: dict[str, list[str]], identifier: str, path: str) -> dict[str, list[str]]:
|
||||
if identifier in redirects:
|
||||
raise IdentifierExists(identifier)
|
||||
|
||||
# Insert the new identifier in alphabetical order
|
||||
new_redirects = list(redirects.items())
|
||||
insertion_index = 0
|
||||
for i, (key, _) in enumerate(new_redirects):
|
||||
if identifier > key:
|
||||
insertion_index = i + 1
|
||||
else:
|
||||
break
|
||||
new_redirects.insert(insertion_index, (identifier, [f"{path}#{identifier}"]))
|
||||
return dict(new_redirects)
|
||||
|
||||
|
||||
def move_content(redirects: dict[str, list[str]], identifier: str, path: str) -> dict[str, list[str]]:
|
||||
if identifier not in redirects:
|
||||
raise IdentifierNotFound(identifier)
|
||||
redirects[identifier].insert(0, f"{path}#{identifier}")
|
||||
return redirects
|
||||
|
||||
|
||||
def rename_identifier(
|
||||
redirects: dict[str, list[str]],
|
||||
old_identifier: str,
|
||||
new_identifier: str
|
||||
) -> dict[str, list[str]]:
|
||||
if old_identifier not in redirects:
|
||||
raise IdentifierNotFound(old_identifier)
|
||||
if new_identifier in redirects:
|
||||
raise IdentifierExists(new_identifier)
|
||||
|
||||
# To minimise the diff, we recreate the redirects mapping allowing
|
||||
# the new key to be updated in-place, preserving the index.
|
||||
new_redirects = {}
|
||||
current_path = ""
|
||||
for key, value in redirects.items():
|
||||
if key == old_identifier:
|
||||
new_redirects[new_identifier] = value
|
||||
current_path = value[0].split('#')[0]
|
||||
continue
|
||||
new_redirects[key] = value
|
||||
new_redirects[new_identifier].insert(0, f"{current_path}#{new_identifier}")
|
||||
return new_redirects
|
||||
|
||||
|
||||
def remove_and_redirect(
|
||||
redirects: dict[str, list[str]],
|
||||
old_identifier: str,
|
||||
new_identifier: str
|
||||
) -> dict[str, list[str]]:
|
||||
if old_identifier not in redirects:
|
||||
raise IdentifierNotFound(old_identifier)
|
||||
if new_identifier not in redirects:
|
||||
raise IdentifierNotFound(new_identifier)
|
||||
redirects[new_identifier].extend(redirects.pop(old_identifier))
|
||||
return redirects
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="redirects manipulation for nixos manuals")
|
||||
commands = parser.add_subparsers(dest="command", required=True)
|
||||
parser.add_argument("-f", "--file", type=Path, required=True)
|
||||
|
||||
add_content_cmd = commands.add_parser("add-content")
|
||||
add_content_cmd.add_argument("identifier", type=str)
|
||||
add_content_cmd.add_argument("path", type=str)
|
||||
|
||||
move_content_cmd = commands.add_parser("move-content")
|
||||
move_content_cmd.add_argument("identifier", type=str)
|
||||
move_content_cmd.add_argument("path", type=str)
|
||||
|
||||
rename_id_cmd = commands.add_parser("rename-identifier")
|
||||
rename_id_cmd.add_argument("old_identifier", type=str)
|
||||
rename_id_cmd.add_argument("new_identifier", type=str)
|
||||
|
||||
remove_redirect_cmd = commands.add_parser("remove-and-redirect")
|
||||
remove_redirect_cmd.add_argument("identifier", type=str)
|
||||
remove_redirect_cmd.add_argument("target_identifier", type=str)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.file) as file:
|
||||
redirects = json.load(file)
|
||||
|
||||
try:
|
||||
if args.command == "add-content":
|
||||
redirects = add_content(redirects, args.identifier, args.path)
|
||||
print(f"Added new identifier: {args.identifier}")
|
||||
|
||||
elif args.command == "move-content":
|
||||
redirects = move_content(redirects, args.identifier, args.path)
|
||||
print(f"Moved '{args.identifier}' to the new path: {args.path}")
|
||||
|
||||
elif args.command == "rename-identifier":
|
||||
redirects = rename_identifier(redirects, args.old_identifier, args.new_identifier)
|
||||
print(f"Renamed identifier from {args.old_identifier} to {args.new_identifier}")
|
||||
|
||||
elif args.command == "remove-and-redirect":
|
||||
redirects = remove_and_redirect(redirects, args.identifier, args.target_identifier)
|
||||
print(f"Redirect from '{args.identifier}' to '{args.target_identifier}' added.")
|
||||
except Exception as error:
|
||||
print(error, file=sys.stderr)
|
||||
else:
|
||||
with open(args.file, "w") as file:
|
||||
json.dump(redirects, file, indent=2)
|
||||
file.write("\n")
|
||||
|
||||
|
||||
class IdentifierExists(Exception):
|
||||
def __init__(self, identifier: str):
|
||||
self.identifier = identifier
|
||||
|
||||
def __str__(self):
|
||||
return f"The identifier '{self.identifier}' already exists."
|
||||
|
||||
|
||||
class IdentifierNotFound(Exception):
|
||||
def __init__(self, identifier: str):
|
||||
self.identifier = identifier
|
||||
|
||||
def __str__(self):
|
||||
return f"The identifier '{self.identifier}' does not exist in the redirect mapping."
|
@ -0,0 +1,16 @@
|
||||
[project]
|
||||
name = "nixos-render-docs-redirects"
|
||||
version = "0.0"
|
||||
description = "redirects manipulation for nixos manuals"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
redirects = "nixos_render_docs_redirects:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,86 @@
|
||||
import unittest
|
||||
from nixos_render_docs_redirects import (
|
||||
add_content,
|
||||
move_content,
|
||||
rename_identifier,
|
||||
remove_and_redirect,
|
||||
IdentifierExists,
|
||||
IdentifierNotFound,
|
||||
)
|
||||
|
||||
|
||||
class RedirectsTestCase(unittest.TestCase):
|
||||
def test_add_content(self):
|
||||
initial_redirects = {
|
||||
"bar": ["path/to/bar.html#bar"],
|
||||
"foo": ["path/to/foo.html#foo"],
|
||||
}
|
||||
final_redirects = {
|
||||
"bar": ["path/to/bar.html#bar"],
|
||||
"baz": ["path/to/baz.html#baz"],
|
||||
"foo": ["path/to/foo.html#foo"],
|
||||
}
|
||||
|
||||
result = add_content(initial_redirects, "baz", "path/to/baz.html")
|
||||
self.assertEqual(list(result.items()), list(final_redirects.items()))
|
||||
|
||||
with self.assertRaises(IdentifierExists):
|
||||
add_content(result, "foo", "another/path.html")
|
||||
|
||||
|
||||
def test_move_content(self):
|
||||
initial_redirects = {
|
||||
"foo": ["path/to/foo.html#foo"],
|
||||
"bar": ["path/to/bar.html#bar"],
|
||||
}
|
||||
final_redirects = {
|
||||
"foo": ["new/path.html#foo", "path/to/foo.html#foo"],
|
||||
"bar": ["path/to/bar.html#bar"],
|
||||
}
|
||||
|
||||
result = move_content(initial_redirects, "foo", "new/path.html")
|
||||
self.assertEqual(list(result.items()), list(final_redirects.items()))
|
||||
|
||||
with self.assertRaises(IdentifierNotFound):
|
||||
move_content(result, "baz", "path.html")
|
||||
|
||||
|
||||
def test_rename_identifier(self):
|
||||
initial_redirects = {
|
||||
"foo": ["path/to/foo.html#foo"],
|
||||
"bar": ["path/to/bar.html#bar"],
|
||||
"baz": ["path/to/baz.html#baz"],
|
||||
}
|
||||
final_redirects = {
|
||||
"foo": ["path/to/foo.html#foo"],
|
||||
"boo": ["path/to/bar.html#boo", "path/to/bar.html#bar"],
|
||||
"baz": ["path/to/baz.html#baz"],
|
||||
}
|
||||
|
||||
result = rename_identifier(initial_redirects, "bar", "boo")
|
||||
self.assertEqual(list(result.items()), list(final_redirects.items()))
|
||||
|
||||
with self.assertRaises(IdentifierNotFound):
|
||||
rename_identifier(result, "bar", "boo")
|
||||
with self.assertRaises(IdentifierExists):
|
||||
rename_identifier(result, "boo", "boo")
|
||||
|
||||
|
||||
def test_remove_and_redirect(self):
|
||||
initial_redirects = {
|
||||
"foo": ["new/path.html#foo", "path/to/foo.html#foo"],
|
||||
"bar": ["path/to/bar.html#bar"],
|
||||
"baz": ["path/to/baz.html#baz"],
|
||||
}
|
||||
final_redirects = {
|
||||
"bar": ["path/to/bar.html#bar", "new/path.html#foo", "path/to/foo.html#foo"],
|
||||
"baz": ["path/to/baz.html#baz"],
|
||||
}
|
||||
|
||||
result = remove_and_redirect(initial_redirects, "foo", "bar")
|
||||
self.assertEqual(list(result.items()), list(final_redirects.items()))
|
||||
|
||||
with self.assertRaises(IdentifierNotFound):
|
||||
remove_and_redirect(result, "foo", "bar")
|
||||
with self.assertRaises(IdentifierNotFound):
|
||||
remove_and_redirect(initial_redirects, "foo", "baz")
|
@ -47,23 +47,49 @@ The first element of an identifier's redirects list must denote its current loca
|
||||
|
||||
If you moved content, add its new location as the first element of the redirects mapping.
|
||||
Please update doc/redirects.json or nixos/doc/manual/redirects.json!
|
||||
""") # TODO: automatically detect if you just missed adding a new location, and make a tool to do that for you
|
||||
""")
|
||||
if self.identifiers_without_redirects:
|
||||
error_messages.append(f"""
|
||||
Identifiers present in the source must have a mapping in the redirects file.
|
||||
- {"\n - ".join(self.identifiers_without_redirects)}
|
||||
|
||||
This can happen when an identifier was added or renamed.
|
||||
Please update doc/redirects.json or nixos/doc/manual/redirects.json!
|
||||
""") # TODO: add tooling in the development shell to do that automatically and point to that command
|
||||
|
||||
Added new content?
|
||||
redirects add-content ❬identifier❭ ❬path❭
|
||||
|
||||
Moved existing content to a different output path?
|
||||
redirects move-content ❬identifier❭ ❬path❭
|
||||
|
||||
Renamed existing identifiers?
|
||||
redirects rename-identifier ❬old-identifier❭ ❬new-identifier❭
|
||||
|
||||
Removed content? Redirect to alternatives or relevant release notes.
|
||||
redirects remove-and-redirect ❬identifier❭ ❬target-identifier❭
|
||||
|
||||
Note that you need to run `nix-shell doc` or `nix-shell nixos/doc/manual` to be able to run this command.
|
||||
""")
|
||||
if self.orphan_identifiers:
|
||||
error_messages.append(f"""
|
||||
Keys of the redirects mapping must correspond to some identifier in the source.
|
||||
- {"\n - ".join(self.orphan_identifiers)}
|
||||
|
||||
This can happen when an identifier was removed or renamed.
|
||||
Please update doc/redirects.json or nixos/doc/manual/redirects.json!
|
||||
""") # TODO: add tooling in the development shell to do that automatically and point to that command
|
||||
|
||||
Added new content?
|
||||
redirects add-content ❬identifier❭ ❬path❭
|
||||
|
||||
Moved existing content to a different output path?
|
||||
redirects move-content ❬identifier❭ ❬path❭
|
||||
|
||||
Renamed existing identifiers?
|
||||
redirects rename-identifier ❬old-identifier❭ ❬new-identifier❭
|
||||
|
||||
Removed content? (good for redirecting deprecations to new content or release notes)
|
||||
redirects remove-and-redirect ❬identifier❭ ❬target-identifier❭
|
||||
|
||||
Note that you need to run `nix-shell doc` or `nix-shell nixos/doc/manual` to be able to run this command.
|
||||
""")
|
||||
|
||||
error_messages.append("NOTE: If your Manual build passes locally and you see this message in CI, you probably need a rebase.")
|
||||
return "\n".join(error_messages)
|
||||
|
Loading…
Reference in New Issue
Block a user