Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@
^\.vscode$
^[.]?air[.]toml$
^scratch\.R$
^\.claude$
1 change: 1 addition & 0 deletions .claude/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
settings.local.json
60 changes: 60 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
## R package development

### Key commands

```
# To run code
Rscript -e "devtools::load_all(); code"

# To run all tests
Rscript -e "devtools::test()"

# To run tests for R/{name.R}
Rscript -e "devtools::test(filter = '{name}')"

# To document the package
Rscript -e "devtools::document()"

# To check pkgdown documentation
Rscript -e "pkgdown::check_pkgdown()"

# To format code
air format .
```

### Coding

* Always run `air format .` after generating code
* Use the base pipe operator (`|>`) not the magrittr pipe (`%>%`)
* Don't use `_$x` or `_$[["x"]]` since dbplyr must work on R 4.1.
* Use `\() ...` for single-line anonymous functions. For all other cases, use `function() {...}`

### Testing

- Tests for `R/{name}.R` go in `tests/testthat/test-{name}.R`.
- All new code should have an accompanying test.
- If there are existing tests, place new tests next to similar existing tests.
- Strive to keep your tests minimal with few comments.

### Documentation

- Every user-facing function should be exported and have roxygen2 documentation.
- Wrap roxygen comments at 80 characters.
- Internal functions should not have roxygen documentation.
- Whenever you add a new documentation topic, also add the topic to `_pkgdown.yml`.
- Use `pkgdown::check_pkgdown()` to check that all topics are included in the reference index.

### Writing

- Use sentence case for headings.
- Use US English.

### Proofreading

If the user asks you to proofread a file, act as an expert proofreader and editor with a deep understanding of clear, engaging, and well-structured writing.

Work paragraph by paragraph, always starting by making a TODO list that includes individual items for each top-level heading.

Fix spelling, grammar, and other minor problems without asking the user. Label any unclear, confusing, or ambiguous sentences with a FIXME comment.

Only report what you have changed.
21 changes: 21 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{

"permissions": {
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"defaultMode": "acceptEdits",
"allow": [
"Bash(find:*)",
"Bash(grep:*)",
"Bash(ls:*)",
"Bash(R:*)",
"Bash(Rscript:*)",
"Bash(rm:*)",
"Bash(air:*)",
"WebFetch(domain:github.com)"
],
"deny": [
"Read(.Renviron)",
"Read(.env)"
]
}
}
156 changes: 156 additions & 0 deletions .claude/skills/tidy-deprecate-function/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
---
name: tidy-deprecate-function
description: Guide for deprecating R functions/arguments. Use when a user asks to deprecate a function or parameter, including adding lifecycle warnings, updating documentation, adding NEWS entries, and updating tests.
---

# Deprecate functions and function arguments

Use this skill when deprecating functions or function parameters in dbplyr.

## Overview

This skill guides you through the complete process of deprecating a function or parameter, ensuring all necessary changes are made consistently:

1. Add deprecation warning using `lifecycle::deprecate_warn()`.
2. Silence deprecation warnings in existing tests.
3. Add lifecycle badge to documentation.
4. Add bullet point to NEWS.md.
5. Create test for deprecation warning.

## Workflow

### Step 1: Determine deprecation version

Read the current version from DESCRIPTION and calculate the deprecation version:

- Current version format: `MAJOR.MINOR.PATCH.9000` (development).
- Deprecation version: Next minor release `MAJOR.(MINOR+1).0`.
- Example: If current version is `2.5.1.9000`, deprecation version is `2.6.0`.

### Step 2: Add `lifecycle::deprecate_warn()` call

Add the deprecation warning to the function:

```r
# For a deprecated function:
function_name <- function(...) {
lifecycle::deprecate_warn("X.Y.0", "function_name()", "replacement_function()")
# rest of function
}

# For a deprecated parameter:
function_name <- function(param1, deprecated_param = deprecated()) {
if (lifecycle::is_present(deprecated_param)) {
lifecycle::deprecate_warn("X.Y.0", "function_name(deprecated_param)")
}
# rest of function
}
```

Key points:

- First argument is the deprecation version string (e.g., "2.6.0").
- Second argument describes what is deprecated (e.g., "function_name(param)").
- Optional third argument suggests replacement.
- Use `lifecycle::is_present()` to check if a deprecated parameter was supplied.

### Step 3: Update tests

Find all existing tests that use the deprecated function or parameter and silence lifecycle warnings. Add at the beginning of test blocks that use the deprecated feature:

```r
test_that("existing test with deprecated feature", {
withr::local_options(lifecycle_verbosity = "quiet")

# existing test code
})
```

Then add a new test to verify the deprecation message in the appropriate test file (usually `tests/testthat/test-{name}.R`):

```r
test_that("function_name(deprecated_param) is deprecated", {
expect_snapshot(. <- function_name(deprecated_param = value))
})
```

You'll need to supply any additional arguments to create a valid call.

Then run the tests and verify they pass.

### Step 4: Update documentation

For function deprecation, add to the description section:

```r
#' @description
#' `r lifecycle::badge("deprecated")`
#'
#' This function is deprecated. Please use [replacement_function()] instead.
```

If the documentation does not already contain `@description`, you will need to add it.

For argument deprecation, add to the appropriate `@param` tag:

```r
#' @param deprecated_param `r lifecycle::badge("deprecated")`
```

When deprecating a function or parameter in favor of a replacement, add old/new examples to the `@examples` section to help users migrate. These should relace all existing examples.

```r
#' @examples
#' # Old:
#' old_function(arg1, arg2)
#' # New:
#' replacement_function(arg1, arg2)
#'
#' # Old:
#' x <- "value"
#' old_function("prefix", x, "suffix")
#' # New:
#' replacement_function("prefix {x} suffix")
```

Key points:

- Use "# Old:" and "# New:" comments to clearly show the transition.
- Include 2-3 practical examples covering common use cases.
- Make examples runnable and self-contained.
- Show how the new syntax differs from the old.

Then re-document the package.

### Step 5: Add NEWS entry

Add a bullet point to the top of the "# dbplyr (development version)" section in NEWS.md:

```markdown
# dbplyr (development version)

* `function_name(parameter)` is deprecated and will be removed in a future
version.
* `function_name()` is deprecated. Use `replacement_function()` instead.
```

Place the entry:

- In the lifecycle subsection if it exists, otherwise at the top level under development version.
- Include the replacement if known.
- Keep entries concise and actionable.

## Implementation checklist

When deprecating a function or parameter, ensure you:

- [ ] Read DESCRIPTION to determine deprecation version.
- [ ] Add `lifecycle::deprecate_warn()` call in the function.
- [ ] Add `withr::local_options(lifecycle_verbosity = "quiet")` to existing tests.
- [ ] Create new test for deprecation warning using `expect_snapshot()`.
- [ ] Run tests to verify everything works.
- [ ] Add lifecycle badge to roxygen documentation.
- [ ] Add migration examples to `@examples` section (for function deprecation).
- [ ] Run `devtools::document()` to update documentation.
- [ ] Add bullet point to NEWS.md.
- [ ] Run `air format .` to format code.
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export(use_ccby_license)
export(use_circleci)
export(use_circleci_badge)
export(use_citation)
export(use_claude_code)
export(use_code_of_conduct)
export(use_conflicted)
export(use_course)
Expand Down
51 changes: 51 additions & 0 deletions R/claude.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#' Configure a project to use Claude Code
#'
#' @description
#' This function sets up a project to use
#' [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
#' Specifically, it:
#'
#' - Creates a `.claude/` directory with a `CLAUDE.md` file containing
#' project-specific instructions for R package development.
#'
#' - Creates a `.claude/settings.json` configuration file with recommended
#' permissions for R package development, including the ability to run R,
#' format with [Air](https://posit-dev.github.io/air/), and run common
#' development tools.
#'
#' - Creates a `.claude/skills/` directory containing various skills found
#' useful by the tidyverse team. All skills have a `tidy-` prefix to avoid
#' clashing with skills that you might provide.
#'
#' - Updates `.claude/.gitignore` to ignore `settings.local.json` (for
#' user-specific settings).
#'
#' @export
#' @examples
#' \dontrun{
#' use_claude_code()
#' }
use_claude_code <- function() {
use_directory(".claude", ignore = TRUE)
copy_claude_directory()
use_git_ignore("settings.local.json", directory = ".claude")

invisible(TRUE)
}

copy_claude_directory <- function() {
source_dir <- path_package("usethis", "claude")
dest_dir <- proj_path(".claude")

source_dirs <- dir_ls(source_dir, recurse = TRUE, type = "directory")
dir_create(path(dest_dir, path_rel(source_dirs, source_dir)))

source_files <- dir_ls(source_dir, recurse = TRUE, type = "file")
for (source_file in source_files) {
rel_path <- path_rel(source_file, source_dir)
dest_file <- path(dest_dir, rel_path)

ui_bullets(c("v" = "Creating {.path {pth(dest_file)}}."))
file_copy(source_file, dest_file, overwrite = TRUE)
}
}
1 change: 1 addition & 0 deletions _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ reference:
- use_standalone
- use_testthat
- use_air
- use_claude_code
- title: Package release
contents:
- use_cran_comments
Expand Down
60 changes: 60 additions & 0 deletions inst/claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
## R package development

### Key commands

```
# To run code
Rscript -e "devtools::load_all(); code"

# To run all tests
Rscript -e "devtools::test()"

# To run tests for R/{name.R}
Rscript -e "devtools::test(filter = '{name}')"

# To document the package
Rscript -e "devtools::document()"

# To check pkgdown documentation
Rscript -e "pkgdown::check_pkgdown()"

# To format code
air format .
```

### Coding

* Always run `air format .` after generating code
* Use the base pipe operator (`|>`) not the magrittr pipe (`%>%`)
* Don't use `_$x` or `_$[["x"]]` since dbplyr must work on R 4.1.
* Use `\() ...` for single-line anonymous functions. For all other cases, use `function() {...}`

### Testing

- Tests for `R/{name}.R` go in `tests/testthat/test-{name}.R`.
- All new code should have an accompanying test.
- If there are existing tests, place new tests next to similar existing tests.
- Strive to keep your tests minimal with few comments.

### Documentation

- Every user-facing function should be exported and have roxygen2 documentation.
- Wrap roxygen comments at 80 characters.
- Internal functions should not have roxygen documentation.
- Whenever you add a new documentation topic, also add the topic to `_pkgdown.yml`.
- Use `pkgdown::check_pkgdown()` to check that all topics are included in the reference index.

### Writing

- Use sentence case for headings.
- Use US English.

### Proofreading

If the user asks you to proofread a file, act as an expert proofreader and editor with a deep understanding of clear, engaging, and well-structured writing.

Work paragraph by paragraph, always starting by making a TODO list that includes individual items for each top-level heading.

Fix spelling, grammar, and other minor problems without asking the user. Label any unclear, confusing, or ambiguous sentences with a FIXME comment.

Only report what you have changed.
21 changes: 21 additions & 0 deletions inst/claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{

"permissions": {
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"defaultMode": "acceptEdits",
"allow": [
"Bash(find:*)",
"Bash(grep:*)",
"Bash(ls:*)",
"Bash(R:*)",
"Bash(Rscript:*)",
"Bash(rm:*)",
"Bash(air:*)",
"WebFetch(domain:github.com)"
Copy link
Member

@jennybc jennybc Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"WebFetch(domain:github.com)"
"WebFetch(domain:github.com)",
"WebFetch(domain:raw.githubusercontent.com)"

I also end up with this one in most projects.

Other candidates I've accrued in local settings recently that might be generally useful (Claude 🫶 the gh cli):

...
      "Bash(cat:*)",
      "Bash(git grep:*)",
      "Bash(gh pr view:*)",
      "Bash(gh issue view:*)",
      "Bash(gh api:*)",
      "Bash(gh pr diff:*)",
      "Bash(gh issue list:*)",
      "WebFetch(domain:cran.r-project.org)",
...

We should probably regard inst/claude/settings.json as the main source of truth, then that will propagate to usethis's .claude/settings.json by applying use_claude_code() to itself.

],
"deny": [
"Read(.Renviron)",
"Read(.env)"
]
}
}
Loading
Loading