Skip to content

Commit

Permalink
Merge pull request #33 from DanChaltiel/32-autoimport-option-for-cent…
Browse files Browse the repository at this point in the history
…ralized-location

Option to add imports in the package-level documentation
  • Loading branch information
DanChaltiel authored Jan 28, 2025
2 parents 55892f8 + b309ae7 commit b9f4a46
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 26 deletions.
5 changes: 5 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ importFrom(dplyr,transmute)
importFrom(dplyr,ungroup)
importFrom(fs,dir_create)
importFrom(fs,file_create)
importFrom(fs,file_delete)
importFrom(fs,file_exists)
importFrom(fs,file_move)
importFrom(fs,path)
importFrom(fs,path_abs)
importFrom(fs,path_dir)
importFrom(fs,path_temp)
importFrom(glue,glue)
Expand All @@ -54,7 +56,10 @@ importFrom(purrr,map_depth)
importFrom(purrr,map_int)
importFrom(purrr,map_lgl)
importFrom(purrr,walk)
importFrom(readr,read_lines)
importFrom(readr,write_lines)
importFrom(rlang,caller_arg)
importFrom(rlang,check_dots_empty)
importFrom(rlang,check_installed)
importFrom(rlang,current_env)
importFrom(rlang,hash)
Expand Down
8 changes: 3 additions & 5 deletions R/ai_parse.R
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ autoimport_parse = function(ref_list, cache_path, use_cache, pkg_name, ns,
imap(function(refs, filename) {
file_hash = hash_file(filename)
filename = basename(filename)
if(verbose>1) cli_inform(c(">"="File {.file {filename}}"))
cache_file = cache[[filename]]
cache_file_hash = if(is.null(cache_file[["..file_hash"]])) "" else cache_file[["..file_hash"]]
if(isTRUE(read_from_cache) && file_hash==cache_file_hash){
if(verbose>1) cli_inform(c(">"="Reading file {.file {filename}} (from cache)"))
rtn_file = cache[[filename]][["..imports"]] %>%
map(~{
if(nrow(.x)>0) .x$ai_source = "cache_file"
.x
})
verb = "Reading from cache"
} else {
if(verbose>1) cli_inform(c(">"="Reading file {.file {filename}} (from file)"))
rtn_file = refs %>%
imap(function(ref, fun_name){
cache_ref = cache_file[[fun_name]]
Expand All @@ -54,14 +54,12 @@ autoimport_parse = function(ref_list, cache_path, use_cache, pkg_name, ns,
}
rtn_ref
})
verb = "Reading from file"
cache[[filename]][["..file_hash"]] <<- file_hash
cache[[filename]][["..imports"]] <<- rtn_file
}
if(verbose>1){
s = rtn_file %>% map_dbl(nrow) %>% sum()
cli_inform(c("!"=verb,
"i"="Found {s} function{?s} to import in {length(rtn_file)}
cli_inform(c("i"="Found {s} function{?s} to import in {length(rtn_file)}
function{?s} or code chunk{?s}."))
}
rtn_file
Expand Down
144 changes: 131 additions & 13 deletions R/ai_write.R
Original file line number Diff line number Diff line change
@@ -1,27 +1,73 @@


#' Take a dataframe from `autoimport_ask()`, a reflist from `autoimport_read()`, and
#' a list of lines from `readr::read_lines()`, and compute for each file what importFrom
#' lines should be removed or inserted.
#' Writes the correct lines in `target_dir` so they can be reviewed in `import_review()`.
#' Returns nothing of use.
#'
#' @importFrom cli cli_h1 cli_inform
#' @importFrom dplyr setdiff
#' @importFrom fs path
#' @importFrom purrr imap map
#' @importFrom stringr str_ends
#' @importFrom tibble tibble
#' @noRd
#' @keywords internal
autoimport_write = function(data_imports, ref_list, lines_list, ignore_package,
pkg_name, target_dir, verbose) {

if(verbose>0) cli_h1("Writing")
if(verbose>1) cli_inform(c(">"="Temporarily writing to {.path {target_dir}}."))
#' @importFrom cli cli_h1 cli_inform
#' @importFrom fs file_delete
autoimport_write = function(data_imports, ref_list, lines_list, location,
ignore_package, pkg_name, target_dir, verbose){

stopifnot(is.data.frame(data_imports))
stopifnot(is.character(data_imports$pkg))
stopifnot(names(ref_list)==names(lines_list))
file_delete(dir(target_dir, full.names=TRUE))

if(location=="function"){
if(verbose>0) cli_h1("Writing at function level")
if(verbose>1) cli_inform(c(">"="Temporarily writing to {.path {target_dir}}."))
.autoimport_write_lvl_fn(data_imports, ref_list, lines_list,
ignore_package, pkg_name, target_dir, verbose)
} else {
if(verbose>0) cli_h1("Writing at package level")
if(verbose>1) cli_inform(c(">"="Temporarily writing to {.path {target_dir}}."))
.autoimport_write_lvl_pkg(data_imports, ref_list, lines_list,
ignore_package, pkg_name, target_dir, verbose)
}
}


#' @noRd
#' @keywords internal
#' @importFrom dplyr filter mutate
#' @importFrom fs path
#' @importFrom glue glue
#' @importFrom stringr str_ends
.autoimport_write_lvl_pkg = function(data_imports, ref_list, lines_list,
ignore_package, pkg_name, target_dir, verbose) {
#merge all functions inserts into one (by setting source_fun)
imports = data_imports |>
filter(!(ignore_package & str_ends(file, "-package.[Rr]"))) |>
mutate(source_fun="package_level") |>
get_inserts(exclude=c("base", "inner", pkg_name)) |>
unlist()
inserts = glue("#' @importFrom {imports}")

cur_package_doc = path("R", paste0(pkg_name, "-package"), ext="R")
new_package_doc = path(target_dir, paste0(pkg_name, "-package"), ext="R")

.copy_package_doc(cur_package_doc, new_package_doc)
.add_autoimport_package_doc(new_package_doc)
.update_package_doc(new_package_doc, inserts)
.remove_fun_lvl_imports(lines_list, target_dir, except=cur_package_doc)
TRUE
}


#' @noRd
#' @keywords internal
#' @importFrom cli cli_inform
#' @importFrom dplyr setdiff
#' @importFrom fs path
#' @importFrom purrr imap map
#' @importFrom stringr str_ends
#' @importFrom tibble tibble
.autoimport_write_lvl_fn = function(data_imports, ref_list, lines_list,
ignore_package, pkg_name, target_dir, verbose) {

# data_imports %>% filter(fun=="writeLines")
# .x %>% filter(fun=="writeLines")
Expand Down Expand Up @@ -76,6 +122,78 @@ autoimport_write = function(data_imports, ref_list, lines_list, ignore_package,
}


# Utils pkg-level -----------------------------------------------------------------------------


#' @noRd
#' @keywords internal
#' @importFrom fs file_exists
#' @importFrom readr read_lines write_lines
.copy_package_doc = function(cur_package_doc, new_package_doc){
if(file_exists(cur_package_doc)){
write_lines(read_lines(cur_package_doc), file=new_package_doc)
}
}

#' @noRd
#' @keywords internal
#' @importFrom cli cli_inform
#' @importFrom fs file_exists
#' @importFrom readr read_lines write_lines
#' @importFrom stringr str_detect
.add_autoimport_package_doc = function(package_doc){
if(!file_exists(package_doc)){
cli_inform("Adding package-level documentation {.path {package_doc}}.")
content = ""
} else {
content = read_lines(package_doc)
}
if(any(str_detect(content, "autoimport namespace: start"))){
return(TRUE)
}

content = c(content, "",
"# The following block is used by autoimport.",
"## autoimport namespace: start",
"## autoimport namespace: end",
"NULL")
write_lines(content, package_doc)
}

#' @noRd
#' @keywords internal
#' @importFrom readr read_lines write_lines
#' @importFrom stringr str_detect
.update_package_doc = function(package_doc, inserts){
content = read_lines(package_doc)
start = str_detect(content, "autoimport namespace: start") |> which()
stop = str_detect(content, "autoimport namespace: end") |> which()
if(length(start)==0) start = length(content)
if(length(stop)==0) stop = length(content)

new_content = c(content[1:start], inserts, content[stop:length(content)])
write_lines(new_content, package_doc)
}

#' remove all `@importFrom` tags from source
#' @importFrom fs path path_abs
#' @importFrom purrr imap
#' @importFrom readr write_lines
#' @importFrom stringr str_starts
#' @noRd
#' @keywords internal
.remove_fun_lvl_imports = function(lines_list, target_dir, except){
lines_list |>
imap(function(lines, filename){
if(path_abs(filename) %in% path_abs(except)) return(FALSE)
target_file = path(target_dir, basename(filename))
rmv = str_starts(lines, "#+' *@importFrom")
new_lines = lines[!rmv]
write_lines(new_lines, target_file)
TRUE
})
}

# Utils ---------------------------------------------------------------------------------------


Expand Down Expand Up @@ -142,7 +260,7 @@ get_lines2 = function(src_ref, imports){
}

#' @param lines result of [read_lines()]
#' @param insert line to insert
#' @param insert lines to insert
#' @param pos insert before this position
#' @noRd
#' @keywords internal
Expand Down
12 changes: 9 additions & 3 deletions R/autoimport.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
#' Automatically compute `@importFrom` tags
#'
#' Automatically read all `R` files and compute appropriate `@importFrom` tags in the roxygen headers.
#' Choose which tags to add in the shiny app [import_review()] afterward.
#' The tags can be added to the source files using the [import_review()] shiny app afterward.
#'
#' @param root Path to the root of the package.
#' @param location Whether to add `@importFrom` dispatched above each function, or centralised at the package level.
#' @param files Files to read. Default to the `R/` folder.
#' @param namespace_file Path to the NAMESPACE file
#' @param description_file Path to the DESCRIPTION file
#' @param use_cache Whether to use the cache system. Can only be "read" or "write".
#' @param ignore_package Whether to ignore files ending with `-package.R`
#' @param verbose The higher, the more output printed. May slow the process a bit.
#' @param ... unused
#'
#' @return Mostly used for side effects. Invisibly returns a dataframe summarizing the function imports, with input arguments as attributes.
#' @export
Expand All @@ -29,16 +31,20 @@
#' @importFrom dplyr setdiff
#' @importFrom fs file_exists path path_dir
#' @importFrom purrr map walk
#' @importFrom rlang check_installed current_env set_names
#' @importFrom rlang check_dots_empty check_installed current_env set_names
#' @importFrom utils sessionInfo
autoimport = function(root=".",
...,
location=c("function", "package"),
files=get_R_dir(root),
namespace_file="NAMESPACE",
description_file="DESCRIPTION",
use_cache=TRUE, ignore_package=TRUE,
verbose=2){
target_dir = get_target_dir()
check_dots_empty()
ns = parse_namespace(namespace_file)
location = match.arg(location)
importlist_path = getOption("autoimport_importlist", path(root, "inst/IMPORTLIST"))
cache_path = get_cache_path(root)
if(file_exists(path(root, namespace_file))) namespace_file = path(root, namespace_file)
Expand Down Expand Up @@ -74,7 +80,7 @@ autoimport = function(root=".",

data_imports = autoimport_ask(data_imports, ns, importlist_path, verbose)

ai_write = autoimport_write(data_imports, ref_list, lines_list,
ai_write = autoimport_write(data_imports, ref_list, lines_list, location,
ignore_package, pkg_name, target_dir, verbose)
if(verbose>0) cli_h1("Finished")

Expand Down
9 changes: 7 additions & 2 deletions R/decision.R
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,26 @@
#' @source inspired by [testthat::snapshot_review()]
#' @export
#' @importFrom cli cli_inform
#' @importFrom dplyr arrange desc
#' @importFrom rlang check_installed
#' @importFrom stringr str_ends
import_review = function(source_path="R/",
output_path=get_target_dir(),
background=getOption("autoimport_background", FALSE)) {
check_installed("shiny", "for `import_review()` to work")
check_installed("diffviewer", "for `import_review()` to work")
data_files=review_files(source_path, output_path)
data_files = review_files(source_path, output_path) |>
arrange(desc(str_ends(old_files, "package.[Rr]")))

if(!any(data_files$changed)){
cli_inform("No changes to review.")
return(invisible(FALSE))
}

go = function(data_files){
review_app(data_files)
data_files %>%
filter(changed) %>%
review_app()
rstudio_tickle()
}

Expand Down
4 changes: 4 additions & 0 deletions inst/IMPORTLIST
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
as_tibble = dplyr
attr = base
check_dots_empty = rlang
desc = dplyr
dir_create = fs
div = shiny
file_exists = fs
filter = dplyr
Expand All @@ -12,11 +14,13 @@ ns_env = rlang
order = base
path = fs
print = base
read_lines = readr
readLines = base
regex = stringr
set_names = rlang
setdiff = base
starts_with = dplyr
unname = base
which = base
write_lines = readr
writeLines = base
8 changes: 7 additions & 1 deletion man/autoimport.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions tests/testthat/helper-init.R
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ expect_not_imported = function(output, pkg, fun){
invisible(faulty)
}

test_autoimport = function(files, bad_ns=FALSE, use_cache=FALSE, root=NULL, verbose=2){
test_autoimport = function(files, bad_ns=FALSE, use_cache=FALSE, root=NULL, ..., verbose=2){
#reset file paths
if(is.null(root)){
dir_source = test_path("source") %>% normalizePath()
Expand Down Expand Up @@ -124,7 +124,8 @@ test_autoimport = function(files, bad_ns=FALSE, use_cache=FALSE, root=NULL, verb
ignore_package=TRUE,
use_cache=use_cache,
namespace_file=ns,
verbose=verbose
verbose=verbose,
...
)

}
Expand Down
Loading

0 comments on commit b9f4a46

Please sign in to comment.