diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index dd41fbe..8ec944a 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,8 +1,6 @@ name: Build & maybe upload PyPI package 'on': push: - branches: - - main tags: - '*' pull_request: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d4a1fc1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,60 @@ +repos: + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.5.26 + hooks: + - id: uv-lock + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-toml + - id: check-yaml + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.4 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format + - repo: local + hooks: + - id: build + name: build + entry: cargo build --verbose + language: rust + pass_filenames: false + - id: fmt + name: fmt + entry: cargo fmt --all -- --check + language: rust + pass_filenames: false + - id: test + name: test + entry: cargo test + language: rust + pass_filenames: false + - id: clippy + name: clippy + entry: cargo clippy + language: rust + pass_filenames: false + - id: repo-map + name: repo_map + entry: uv + language: system + pass_filenames: false + args: + - run + - -m + - repo_mapper + - --repo-root + - . + - --readme-path + - ./README.md + - --gitignore-path + - ./.gitignore + - --allowed-exts + - py,rs,md,yaml,toml,lock + - --ignore-dirs + - .venv,target,.git + # - --ignore-hidden diff --git a/Cargo.lock b/Cargo.lock index 3385bc0..a639efd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + [[package]] name = "colored" version = "3.0.0" @@ -204,12 +210,13 @@ dependencies = [ [[package]] name = "readme-update" -version = "0.1.1" +version = "0.2.0" dependencies = [ "colored", "pyo3", "rayon", "regex", + "test-case", "walkdir", ] @@ -268,6 +275,39 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "test-case-core", +] + [[package]] name = "unicode-ident" version = "1.0.18" diff --git a/Cargo.toml b/Cargo.toml index be3eaeb..8be6671 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,17 @@ [package] name = "readme-update" -version = "0.1.2" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "readme_update" -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [dependencies] colored = "3.0.0" pyo3 = "0.25.0" rayon = "1.10.0" regex = "1.11.1" +test-case = "3.3.1" walkdir = "2.5.0" diff --git a/README.md b/README.md index 45b2cd2..cbe5b61 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# Tired of updating documentation? +# readme-update 🦀 +[![PyPI Downloads](https://static.pepy.tech/badge/readme-update)](https://pepy.tech/projects/readme-update) + +## Tired of updating documentation? This tool updates your `README.md` with a one line description for each of the python scripts in a directory you point it to (and recursively). It adds the text for lines that start with `"Description: "` and `"Link: "`. It ignores any that don't have the description. The idea is that links should link to higher level documentation (if it exists). @@ -15,15 +18,18 @@ It will update in place if the `# Scripts` block exists or else it will append i | `example1.py` | This is an example file that links to my own github. | [Link](https://github.com/second-ed) | | `example2.py` | Some other description. | | | `example3.py` | | [Link](https://doc.rust-lang.org/book/) | -| `example_usage.py` | | | :: -# To install the package +# Installation ```shell pip install readme-update ``` +Or +```shell +uv add readme-update +``` -# Usage +# Usage Assuming its is run from this location. ```shell root/ @@ -32,15 +38,58 @@ root/ README.md ``` -# example_script.py -```python -import readme_update -from pathlib import Path +```shell +uv run -m readme-update \ +--scripts-root "./scripts" \ +--readme-path "./README.md" +``` + -path = Path(__file__) +# Args +| Argument | Type | Required | Default | Description | +| ------------------ | --------------------- | -------- | ------- | ---------------------------------------------------- | +| `--scripts-root` | `str` | ✅ | | Path to the root of the scripts to scan | +| `--readme-path` | `str` | ❌ | `'./README.md'` | Path to the README file that will be modified | -readme_update.py_main( - str(path.parent), - str(path.parents[1] / "README.md") -) + +# Ret codes +| RetCode | int | description | +| ----------------------| --- | --------------------- | +| `NoModification` | 0 | The Repo Map reflects the current state of the repo. | +| `ModifiedReadme` | 1 | The README was updated. | +| `NoPyFiles` | 2 | No python files found at the `scripts-root` location. | +| `FailedParsingFile` | 3 | Failed to read README file | +| `FailedToWriteReadme` | 4 | The given `README.md` path does not match the expected basename. | + + +# Repo map +``` +├── .github +│ └── workflows +│ ├── ci.yaml +│ └── publish.yaml +├── python +│ └── update_readme +│ ├── __init__.py +│ └── __main__.py +├── scripts +│ ├── example1.py +│ ├── example2.py +│ └── example3.py +├── src +│ ├── core +│ │ ├── adapters.rs +│ │ ├── domain.rs +│ │ └── mod.rs +│ ├── api.rs +│ └── lib.rs +├── tests +│ └── integration_tests.rs +├── .pre-commit-config.yaml +├── Cargo.lock +├── Cargo.toml +├── README.md +├── pyproject.toml +└── uv.lock +:: ``` diff --git a/pyproject.toml b/pyproject.toml index ae5fa07..e81fe62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "readme-update" -version = "0.1.2" +version = "0.2.0" readme = "README.md" requires-python = ">=3.10" classifiers = [ @@ -12,14 +12,25 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] -dynamic = ["version"] license = { text = "MIT" } dependencies = [] + [tool.maturin] features = ["pyo3/extension-module"] +module-name = "readme_update" +python-source = "python" +python-packages = ["update_readme"] +strip = true [tool.uv] # Rebuild package when any rust files change cache-keys = [{file = "pyproject.toml"}, {file = "rust/Cargo.toml"}, {file = "**/*.rs"}] + +[dependency-groups] +dev = [ + "pre-commit>=4.2.0", + "repo-mapper-rs>=0.1.0", + "ruff>=0.12.5", +] # Uncomment to build rust code in development mode -# config-settings = { build-args = '--profile=dev' } \ No newline at end of file +# config-settings = { build-args = '--profile=dev' } diff --git a/python/update_readme/__init__.py b/python/update_readme/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/update_readme/__main__.py b/python/update_readme/__main__.py new file mode 100644 index 0000000..60a5a3f --- /dev/null +++ b/python/update_readme/__main__.py @@ -0,0 +1,22 @@ +import argparse +import os +import sys +import readme_update + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "--scripts-root", + type=os.path.abspath, + required=True, + help="Path to the root of the scripts to generate the table for.", + ) + parser.add_argument( + "--readme-path", + type=os.path.abspath, + default="./README.md", + help="Path to the readme file.", + ) + args = parser.parse_args() + sys.exit(int(readme_update.py_main(args.scripts_root, args.readme_path))) diff --git a/scripts/example_usage.py b/scripts/example_usage.py deleted file mode 100644 index 7ecc0e2..0000000 --- a/scripts/example_usage.py +++ /dev/null @@ -1,12 +0,0 @@ -"""This is how you'd use the tool.""" - -import readme_update -from pathlib import Path - -path = Path(__file__) - -readme_update.py_main( - str(path.parent), - str(path.parents[1] / "README.md") -) - diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..6e342ce --- /dev/null +++ b/src/api.rs @@ -0,0 +1,22 @@ +use crate::core::adapters::RealFileSystem; +use crate::core::domain::{main, RetCode}; +use pyo3::prelude::*; +use std::path::Path; + +#[pyfunction] +fn py_main(scripts_root: String, readme_path: String) -> PyResult { + let mut file_sys = RealFileSystem; + match main(&mut file_sys, scripts_root, Path::new(&readme_path)) { + RetCode::NoModification => Ok(0), + RetCode::ModifiedReadme => Ok(1), + RetCode::NoPyFiles => Ok(2), + RetCode::FailedParsingFile => Ok(3), + RetCode::FailedToWriteReadme => Ok(4), + } +} + +#[pymodule] +fn readme_update(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(py_main, m)?)?; + Ok(()) +} diff --git a/src/core/adapters.rs b/src/core/adapters.rs new file mode 100644 index 0000000..b7d128a --- /dev/null +++ b/src/core/adapters.rs @@ -0,0 +1,81 @@ +use std::{ + collections::HashMap, + ffi::OsStr, + fs, io, + path::{Path, PathBuf}, +}; + +use walkdir::WalkDir; + +pub trait FileSystem { + fn list_py_files(&self, path: impl AsRef) -> Vec; + fn read_to_string(&self, path: &Path) -> io::Result; + fn write(&mut self, path: &Path, contents: &str) -> std::result::Result<(), std::io::Error>; +} + +pub struct RealFileSystem; + +impl FileSystem for RealFileSystem { + fn list_py_files(&self, path: impl AsRef) -> Vec { + WalkDir::new(path) + .into_iter() + .filter_map(Result::ok) + .filter(|entry| entry.path().extension() == Some(OsStr::new("py"))) + .map(|entry| entry.path().to_path_buf()) + .collect() + } + + fn read_to_string(&self, path: &Path) -> io::Result { + fs::read_to_string(path) + } + + fn write(&mut self, path: &Path, contents: &str) -> std::result::Result<(), std::io::Error> { + fs::write(path, contents) + } +} + +pub struct FakeFileSystem { + pub files: HashMap, + pub operations: Vec, +} + +impl FakeFileSystem { + pub fn new(files: HashMap) -> Self { + Self { + files, + operations: Vec::new(), + } + } +} + +impl Default for FakeFileSystem { + fn default() -> Self { + Self::new(HashMap::new()) + } +} + +impl FileSystem for FakeFileSystem { + fn list_py_files(&self, _path: impl AsRef) -> Vec { + self.files + .keys() + .filter(|p| p.extension() == Some(OsStr::new("py"))) + .map(|p| p.to_path_buf()) + .collect() + } + + fn read_to_string(&self, path: &Path) -> io::Result { + if let Some(contents) = self.files.get(path) { + Ok(contents.to_owned()) + } else { + Err(io::Error::new(io::ErrorKind::NotFound, "File not found")) + } + } + + fn write(&mut self, path: &Path, contents: &str) -> std::result::Result<(), std::io::Error> { + self.operations + .push(format!("write: `{}`", &path.display())); + self.files + .insert(path.to_path_buf(), contents.to_string().clone()); + Ok(()) + } +} diff --git a/src/core/domain.rs b/src/core/domain.rs new file mode 100644 index 0000000..6234bd8 --- /dev/null +++ b/src/core/domain.rs @@ -0,0 +1,240 @@ +use crate::core::adapters::FileSystem; +use colored::Colorize; +use rayon::prelude::*; +use regex::Regex; +use std::{ + ffi::OsStr, + io, + path::{Path, PathBuf}, +}; + +pub fn main(file_sys: &mut impl FileSystem, scripts_root: String, readme_path: &Path) -> RetCode { + let readme = match ReadMe::parse(file_sys, readme_path) { + Ok(r) => r, + Err(e) => { + eprintln!("{} {}", "Failed to read README file: ".red().bold(), e); + return RetCode::FailedParsingFile; + } + }; + + let paths = file_sys.list_py_files(&scripts_root); + if paths.is_empty() { + println!( + "{} `{}`", + "No files to analyse at path: ".red().bold(), + scripts_root.clone().yellow() + ); + return RetCode::NoPyFiles; + } + + let py_files: Vec = extract_pyfiles(file_sys, paths); + let scripts_docs = generate_scripts_docs(py_files); + let modified_readme = update_readme(&readme, scripts_docs); + + if modified_readme != readme { + if let Err(e) = modified_readme.write(file_sys, readme_path) { + eprintln!("{} {}", "Failed to write README file: ".red().bold(), e); + return RetCode::FailedToWriteReadme; + }; + println!("{}", "Modified README.md".yellow().bold()); + return RetCode::ModifiedReadme; + } + println!("{}", "Nothing to modify".green().bold()); + RetCode::NoModification +} + +#[derive(Debug, PartialEq, Eq)] +pub enum RetCode { + NoModification, + ModifiedReadme, + NoPyFiles, + FailedParsingFile, + FailedToWriteReadme, +} + +#[derive(Debug, Eq, PartialEq)] +struct ReadMe(String); + +impl ReadMe { + pub fn parse(file_sys: &mut impl FileSystem, path: &Path) -> Result { + let allowed_exts = ["md", "rst", "txt"]; + let ext = path + .extension() + .and_then(OsStr::to_str) + .unwrap_or("") + .to_ascii_lowercase(); + + let valid_ext = allowed_exts.contains(&ext.as_str()); + + let valid_file_name = path + .file_name() + .and_then(OsStr::to_str) + .map(|name| name.to_ascii_uppercase().contains("README")) + .unwrap_or(false); + + if valid_file_name && valid_ext { + file_sys.read_to_string(path).map(ReadMe) + } else { + Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "File name does not contain `README` or is not valid extension in {:?}", + allowed_exts + ), + )) + } + } + + pub fn write(&self, file_sys: &mut impl FileSystem, path: &Path) -> Result<(), io::Error> { + file_sys.write(path, &self.0) + } +} + +#[derive(Debug)] +struct PyFile { + pub path: PathBuf, + _code: String, + docstring: String, +} + +impl PyFile { + fn new(path: impl AsRef, code: &str, docstring: &str) -> Self { + let path = path.as_ref().to_path_buf(); + Self { + path, + _code: code.to_string(), + docstring: docstring.to_string(), + } + } +} + +fn extract_pyfiles(file_sys: &impl FileSystem, paths: Vec) -> Vec { + paths + .into_iter() + .filter_map(|path| { + file_sys.read_to_string(&path).ok().as_ref().map(|code| { + let docstring = extract_module_docstring(code); + PyFile::new(path, code, &docstring) + }) + }) + .collect() +} + +fn extract_module_docstring(code: &str) -> String { + let pattern = Regex::new(r#"(?s)\A[ \t]*(?i:r|u)?"""(.*?)"""#).unwrap(); + pattern + .captures(code) + .and_then(|caught| caught.get(1).map(|m| m.as_str().to_string())) + .unwrap_or_default() +} + +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct DocInfo { + path: PathBuf, + desc: String, + link: String, +} + +impl DocInfo { + pub fn to_readme(&self) -> String { + let basename: String = self.path.file_name().unwrap().to_string_lossy().to_string(); + format!("| `{}` | {} | {} |", basename, self.desc, self.link) + } +} + +fn extract_docinfo(py_files: Vec) -> Vec { + let mut doc_infos: Vec = py_files + .into_par_iter() + .map(|py_file| { + let mut desc = String::new(); + let mut link = String::new(); + + for line in py_file.docstring.lines() { + let trimmed_line = line.trim_start(); + + if let Some(rest) = trimmed_line.strip_prefix("Description: ") { + desc = rest.to_string(); + } else if let Some(rest) = trimmed_line.strip_prefix("Link: ") { + link = format!("[Link]({})", rest); + } + } + DocInfo { + path: py_file.path, + desc, + link, + } + }) + .collect(); + doc_infos.par_sort_by_key(|s| s.path.clone()); + doc_infos +} + +fn create_readme(doc_infos: Vec) -> String { + [ + "# Scripts", + "| Name | Description | Link |", + "|:---|:---|:---|", + ] + .into_iter() + .map(str::to_string) + .chain(doc_infos.iter().map(|n| n.to_readme()).collect::>()) + .chain(std::iter::once("::".to_string())) + .collect::>() + .join("\n") +} + +fn generate_scripts_docs(py_files: Vec) -> String { + create_readme(extract_docinfo(py_files)) +} + +fn update_readme(readme: &ReadMe, scripts_docs: String) -> ReadMe { + let pattern = Regex::new(r"(?s)(?m)^# Scripts.*?^::").expect("valid regex"); + + let updated = if pattern.is_match(&readme.0) { + pattern.replace(&readme.0, scripts_docs).into_owned() + } else { + format!("{}\n\n{}", readme.0, scripts_docs) + }; + ReadMe(updated) +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_scripts_docs() { + let py_files = vec![ + PyFile::new( + "some/python/file1.py", + "", + "Description: This is a description\n\nLink: some_link.com/link1", + ), + PyFile::new( + "some/python/file3.py", + "", + "missing description start\n\nLink: some_other_link.com/link2", + ), + PyFile::new( + "some/python/file2.py", + "", + "Description: This is another description\n\n", + ), + ]; + + let expected_readme = [ + "# Scripts", + "| Name | Description | Link |", + "|:---|:---|:---|", + "| `file1.py` | This is a description | [Link](some_link.com/link1) |", + "| `file2.py` | This is another description | |", + "| `file3.py` | | [Link](some_other_link.com/link2) |", + "::", + ] + .into_iter() + .map(str::to_string) + .collect::>() + .join("\n"); + + assert_eq!(generate_scripts_docs(py_files), expected_readme); + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..cc0e7e0 --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,2 @@ +pub mod adapters; +pub mod domain; diff --git a/src/lib.rs b/src/lib.rs index e88148f..b3442cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,271 +1,2 @@ -use std::{path::Path, process::ExitCode}; - -use pyo3::prelude::*; - -use crate::core::main; - -#[pyfunction] -fn py_main(scripts_root: String, readme_path: String) -> PyResult { - match main(scripts_root, Path::new(&readme_path)) { - ExitCode::SUCCESS => Ok(0), - ExitCode::FAILURE => Ok(1), - _ => Ok(-1), - } -} - -#[pymodule] -fn readme_update(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(py_main, m)?)?; - Ok(()) -} - -mod core { - use colored::Colorize; - use rayon::prelude::*; - use regex::Regex; - use std::{ - ffi::OsStr, - fs, io, - ops::Deref, - path::{Path, PathBuf}, - process::ExitCode, - }; - use walkdir::WalkDir; - - pub fn main(scripts_root: String, readme_path: &Path) -> ExitCode { - let readme = match ReadMeString::read(readme_path) { - Ok(r) => r, - Err(e) => { - eprintln!("{} {}", "Failed to read README file: ".red().bold(), e); - return ExitCode::FAILURE; - } - }; - - let paths = list_files(&scripts_root); - if paths.is_empty() { - println!( - "{} `{}`", - "No files to analyse at path: ".red().bold(), - scripts_root.clone().yellow() - ); - return ExitCode::FAILURE; - } - - let py_files: Vec = extract_pyfiles(paths); - let scripts_docs = generate_scripts_docs(py_files); - let modified_readme = update_readme(&readme, scripts_docs); - - if modified_readme != readme { - if let Err(e) = modified_readme.write(readme_path) { - eprintln!("{} {}", "Failed to write README file: ".red().bold(), e); - return ExitCode::FAILURE; - }; - println!("{}", "Modified README.md".yellow().bold()); - return ExitCode::FAILURE; - } - println!("{}", "Nothing to modify".green().bold()); - ExitCode::SUCCESS - } - - #[derive(Debug, Eq, PartialEq)] - struct ReadMeString(String); - - impl ReadMeString { - pub fn read(path: &Path) -> Result { - let allowed_exts = ["md", "rst", "txt"]; - let ext = path - .extension() - .and_then(OsStr::to_str) - .unwrap_or("") - .to_ascii_lowercase(); - - let valid_ext = allowed_exts.contains(&ext.as_str()); - - let valid_file_name = path - .file_name() - .and_then(OsStr::to_str) - .map(|name| name.to_ascii_uppercase().contains("README")) - .unwrap_or(false); - - if valid_file_name && valid_ext { - fs::read_to_string(path).map(ReadMeString) - } else { - Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "File name does not contain `README` or is not valid extension in {:?}", - allowed_exts - ), - )) - } - } - - pub fn write(&self, path: &Path) -> Result<(), io::Error> { - fs::write(path, &self.0) - } - } - - impl Deref for ReadMeString { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - pub fn list_files(path: &String) -> Vec { - WalkDir::new(path) - .into_iter() - .filter_map(Result::ok) - .filter(|entry| entry.path().extension() == Some(OsStr::new("py"))) - .map(|entry| entry.path().to_path_buf()) - .collect() - } - - #[derive(Debug)] - struct PyFile { - pub path: PathBuf, - code: String, - docstring: String, - } - - impl PyFile { - fn new(path: PathBuf, code: &str, docstring: &str) -> Self { - Self { - path, - code: code.to_string(), - docstring: docstring.to_string(), - } - } - } - - fn extract_pyfiles(paths: Vec) -> Vec { - paths - .into_par_iter() - .filter_map(|path| { - fs::read_to_string(&path).ok().as_ref().map(|code| { - let docstring = extract_module_docstring(code); - PyFile::new(path, code, &docstring) - }) - }) - .collect() - } - - fn extract_module_docstring(code: &str) -> String { - let pattern = Regex::new(r#"(?s)\A[ \t]*(?i:r|u)?"""(.*?)"""#).unwrap(); - pattern - .captures(code) - .and_then(|caught| caught.get(1).map(|m| m.as_str().to_string())) - .unwrap_or_default() - } - #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] - pub struct DocInfo { - path: PathBuf, - desc: String, - link: String, - } - - impl DocInfo { - pub fn to_readme(&self) -> String { - let basename: String = self.path.file_name().unwrap().to_string_lossy().to_string(); - format!("| `{}` | {} | {} |", basename, self.desc, self.link) - } - } - - fn extract_docinfo(py_files: Vec) -> Vec { - let mut doc_infos: Vec = py_files - .into_par_iter() - .map(|py_file| { - let mut desc = String::new(); - let mut link = String::new(); - - for line in py_file.docstring.lines() { - let trimmed_line = line.trim_start(); - - if let Some(rest) = trimmed_line.strip_prefix("Description: ") { - desc = rest.to_string(); - } else if let Some(rest) = trimmed_line.strip_prefix("Link: ") { - link = format!("[Link]({})", rest); - } - } - DocInfo { - path: py_file.path, - desc, - link, - } - }) - .collect(); - doc_infos.par_sort_by_key(|s| s.path.clone()); - doc_infos - } - - fn create_readme(doc_infos: Vec) -> String { - [ - "# Scripts", - "| Name | Description | Link |", - "|:---|:---|:---|", - ] - .into_iter() - .map(str::to_string) - .chain(doc_infos.iter().map(|n| n.to_readme()).collect::>()) - .chain(std::iter::once("::".to_string())) - .collect::>() - .join("\n") - } - - fn generate_scripts_docs(py_files: Vec) -> String { - create_readme(extract_docinfo(py_files)) - } - - fn update_readme(readme: &ReadMeString, scripts_docs: String) -> ReadMeString { - let pattern = Regex::new(r"(?s)(?m)^# Scripts.*?^::").expect("valid regex"); - - let updated = if pattern.is_match(&readme.0) { - pattern.replace(&readme.0, scripts_docs).into_owned() - } else { - format!("{}\n\n{}", readme.0, scripts_docs) - }; - ReadMeString(updated) - } - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn test_generate_scripts_docs() { - let py_files = vec![ - PyFile::new( - PathBuf::from("some/python/file1.py"), - "", - "Description: This is a description\n\nLink: some_link.com/link1", - ), - PyFile::new( - PathBuf::from("some/python/file3.py"), - "", - "missing description start\n\nLink: some_other_link.com/link2", - ), - PyFile::new( - PathBuf::from("some/python/file2.py"), - "", - "Description: This is another description\n\n", - ), - ]; - - let expected_readme = [ - "# Scripts", - "| Name | Description | Link |", - "|:---|:---|:---|", - "| `file1.py` | This is a description | [Link](some_link.com/link1) |", - "| `file2.py` | This is another description | |", - "| `file3.py` | | [Link](some_other_link.com/link2) |", - "::", - ] - .into_iter() - .map(str::to_string) - .collect::>() - .join("\n"); - - assert_eq!(generate_scripts_docs(py_files), expected_readme); - } - } -} +pub mod api; +pub mod core; diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 0000000..5753e9f --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,54 @@ +use readme_update::core::{ + adapters::FakeFileSystem, + domain::{main, RetCode}, +}; +use std::{collections::HashMap, path::PathBuf}; +use test_case::test_case; + +#[test_case( + "repo/root/scripts", "repo/root/README.md", + "# Some readme\n\n# Scripts\n| Name | Description | Link |\n|:---|:---|:---|\n| `file1.py` | some desc | [Link](some-link.com) |\n::", + RetCode::NoModification, + "# Some readme\n\n# Scripts\n| Name | Description | Link |\n|:---|:---|:---|\n| `file1.py` | some desc | [Link](some-link.com) |\n::" +)] +#[test_case( + "repo/root/scripts", "repo/root/README.md", "# Some readme", + RetCode::ModifiedReadme, + "# Some readme\n\n# Scripts\n| Name | Description | Link |\n|:---|:---|:---|\n| `file1.py` | some desc | [Link](some-link.com) |\n::" +)] +#[test_case( + "repo/root/scripts", + "repo/root/INVALID.md", + "# Some readme", + RetCode::FailedParsingFile, + "# Some readme" +)] +fn test_readme_update( + scripts_root: &str, + readme_path: &str, + readme_str: &str, + expected_ret_code: RetCode, + expected_readme: &str, +) { + let scripts_root = scripts_root.to_string(); + + let files: HashMap = vec![ + ( + "repo/root/scripts/file1.py", + "\"\"\"Description: some desc\n\nLink: some-link.com\"\"\"", + ), + (readme_path, readme_str), + ] + .into_iter() + .map(|(k, v)| (PathBuf::from(k), v.to_string())) + .collect::>(); + let mut file_sys = FakeFileSystem::new(files); + + assert_eq!( + main(&mut file_sys, scripts_root, &PathBuf::from(readme_path)), + expected_ret_code + ); + + let actual_readme = file_sys.files.get(&PathBuf::from(readme_path)); + assert_eq!(actual_readme, Some(&expected_readme.to_string())); +} diff --git a/uv.lock b/uv.lock index 564c7ba..0a22895 100644 --- a/uv.lock +++ b/uv.lock @@ -1,11 +1,207 @@ version = 1 -revision = 1 requires-python = ">=3.10" -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version < '3.11'", + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047 }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988 }, +] + +[[package]] +name = "identify" +version = "2.6.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, +] + +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, ] [[package]] name = "readme-update" +version = "0.2.0" source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pre-commit" }, + { name = "repo-mapper-rs" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pre-commit", specifier = ">=4.2.0" }, + { name = "repo-mapper-rs", specifier = ">=0.1.0" }, + { name = "ruff", specifier = ">=0.12.5" }, +] + +[[package]] +name = "repo-mapper-rs" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/97/88/7025225dc64f08218a15e1f36a3da694f0b513b88a04132fca356f60ba67/repo_mapper_rs-0.3.0.tar.gz", hash = "sha256:c319087a78930977a53a4322c58c90cd1c016fc2bb2a6b2b097efd2c2cf71297", size = 23703 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/31/5a306af5cb4d79e26524ff0f69377074eb2278cff0c1e86201eb429f40a6/repo_mapper_rs-0.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f044b9d78ab1d8f357d0515ed6e724741d7585c8bbc93e4d2cc511fddc24097c", size = 780453 }, + { url = "https://files.pythonhosted.org/packages/d9/08/c9a80f13a59375e2b3e84934c4973e77b4f3305238429a1c6bbf41548f80/repo_mapper_rs-0.3.0-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:ffa756932d5cb1e71e231aec375e49a8f48f270c59e98e61e16d1c5b299ff7d4", size = 924112 }, + { url = "https://files.pythonhosted.org/packages/dd/9e/e9758b661147a3327d5166915be4fc88c877e698a207ee95d0c31e1ca479/repo_mapper_rs-0.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:d420b8b9a59f67d7b437aa3257c8957a4d52fc6d07839cf88aafe16511fa56cc", size = 789305 }, + { url = "https://files.pythonhosted.org/packages/f6/6c/286c9735bc7da077a4ddd263ae99f92f3cddaec055968f1ae427b45c5d34/repo_mapper_rs-0.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9ed724adbec2bf1d4cd6851bfea2908f268bfbf68cb9ec89348e02a22ed9a", size = 780389 }, + { url = "https://files.pythonhosted.org/packages/af/b8/2f26675ca29f65b74d63084db912b4c2bae9519f8ba4e2d7fe3919ce1930/repo_mapper_rs-0.3.0-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:a45e0605fe01794981523a6e4a50a5cd08da88e7b2e58dc18ad583bb8625477d", size = 924036 }, + { url = "https://files.pythonhosted.org/packages/f3/a3/8dd90828132bace409078acad5492b6fb595a12b657a0e2803dc3af401e2/repo_mapper_rs-0.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:2bf513347169cd3d6b8ecbd4c2aed9fd359239ff301bb387075fde327e6726a5", size = 789264 }, + { url = "https://files.pythonhosted.org/packages/37/5e/b82d2a4235fd0c7bc75cf04fd688f80f39b52c485443f6aabafa8ffd1049/repo_mapper_rs-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:354504aef5d2eeefffc50a267146a6a0120ec35b37864772a5ef6c594a15ad70", size = 776755 }, + { url = "https://files.pythonhosted.org/packages/48/e8/5af0af9be629fad4ed76747346e815c707baa1b243d387d7fd79b064c2d6/repo_mapper_rs-0.3.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:7cd2373714a0901c53d7d85cb657a020e385d13c0781bcc7b5cd4dfc490969d0", size = 922475 }, + { url = "https://files.pythonhosted.org/packages/3f/ad/a3de567a59cf3f4cce6b52b9ef8ae289a39502b9e71d4242cff16a4754db/repo_mapper_rs-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:e961b3a4b17d615a3ac0b9e55308c3fe51ad0159ee97217ee145d5e2439a2ca0", size = 788757 }, + { url = "https://files.pythonhosted.org/packages/4f/45/200104e672f43d263b15b1107314d999ebf7145bea148f0cd102c289480f/repo_mapper_rs-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:80588563197422068cbb5b0c3cb23811f7dc4fc04ab03d5bcada2fe19c010700", size = 776607 }, + { url = "https://files.pythonhosted.org/packages/0a/72/9fafdee20f496a8cc71b622c7c3dbbc9231423e7f4e0a0f43218f78b7fc6/repo_mapper_rs-0.3.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:e73c9335348aac980e7f25f44d4ba1be87b065855151411a0402cb720c564b61", size = 921899 }, + { url = "https://files.pythonhosted.org/packages/1b/14/dedd1b243fdc2a16b291d9b3a664ee593bff997f1dcb5eaffedf5b776ade/repo_mapper_rs-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:c0815eff543c46a051b93247e98bad42213333d47415367b4d0716b4a01e88ef", size = 787923 }, +] + +[[package]] +name = "ruff" +version = "0.12.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161 }, + { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884 }, + { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754 }, + { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276 }, + { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700 }, + { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783 }, + { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642 }, + { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107 }, + { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521 }, + { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528 }, + { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443 }, + { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759 }, + { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463 }, + { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603 }, + { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356 }, + { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089 }, + { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616 }, + { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225 }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, +] + +[[package]] +name = "virtualenv" +version = "20.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279 }, +]