Skip to content

Conversation

@hadley
Copy link
Member

@hadley hadley commented Dec 31, 2025

Fixes #2302

@claude
Copy link

claude bot commented Dec 31, 2025

Claude finished @hadley's task —— View job


PR Review: Enhance OO mocking

Reviewed the OO mocking enhancements. Found one potential issue:

Issue in remove_s3_method() (R/mock-oo.R:66-69):

if (env_has(s3_envir, ".__S3MethodsTable__.")) {
  table <- env_get(s3_envir, ".__S3MethodsTable__.")
  env_unbind(table, paste0(generic, ".", class))
}

The function should check if the method exists in the table before attempting to unbind it. Calling env_unbind() on a non-existent key could cause issues.

Suggested fix:

if (env_has(s3_envir, ".__S3MethodsTable__.")) {
  table <- env_get(s3_envir, ".__S3MethodsTable__.")
  method_name <- paste0(generic, ".", class)
  if (env_has(table, method_name)) {
    env_unbind(table, method_name)
  }
}

Otherwise, the changes look solid with good test coverage.

@hadley hadley requested a review from DavisVaughan January 6, 2026 23:15
R/mock-oo.R Outdated
check_function(definition)
check_function(definition, allow_null = TRUE)

generic_def <- get(generic, envir = frame)
Copy link
Member

Choose a reason for hiding this comment

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

Unused. Remove?

Comment on lines +62 to +63
genfun <- get(generic, envir = envir)
s3_envir <- environment(genfun) %||% .BaseNamespaceEnv
Copy link
Member

Choose a reason for hiding this comment

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

i noticed that registerS3Method is slightly safer here, checking typeof(genfun) first before calling environment() on it

        genfun <- get(genname, envir = envir)
        ....
        if (typeof(genfun) == "closure") 
            environment(genfun)
        else .BaseNamespaceEnv

(Maybe environment() never fails, even with weird input, im not 100% sure)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I skipped that because the environment of a "special" is NULL.

R/mock-oo.R Outdated
check_function(definition, allow_null = TRUE)

generic_def <- get(generic, envir = frame)

Copy link
Member

Choose a reason for hiding this comment

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

I noticed that the S4 bit checks that the generic exists first, but the S3 bit does not. Should we enforce that the S3 generic exists before doing any work?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think it's worth it, as there's no simpler equivalent of getGeneric() for S3 generics and the existing error message is ok.

R/mock-oo.R Outdated
Comment on lines 89 to 101
set_method <- function(def) {
env <- topenv(frame)
old <- methods::getMethod(generic_def, signature, optional = TRUE)
if (is.null(def)) {
methods::removeMethod(generic_def, signature, env)
} else {
suppressMessages(methods::setMethod(generic_def, signature, def, env))
}
old
}

old <- set_method(definition)
withr::defer(set_method(old), frame)
Copy link
Member

Choose a reason for hiding this comment

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

Was it purposeful that set_method() locally captures generic_def and signature from the surrounding env rather than making them arguments? That kind of local capture always feels like a code smell to me (or when I need it I drop a comment calling it out). I would have rather seen set_method(generic_def, signature, def) at each call site, where you see the full set of info required to set the method.

At the very least, I had to think about it for a minute when I saw generic_def in there but didn't see it defined anywhere, and had to backtrack up the outer function to find it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, because otherwise the calls are too long to fit nicely on one line. But maybe I refactored more since that was a problem.

You don't think having a function definition inside another function is a strong indicator that you're using local state?

Copy link
Member

Choose a reason for hiding this comment

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

You don't think having a function definition inside another function is a strong indicator that you're using local state

I guess that's true, I just see it so rarely that it trips my brain up for a minute

Copy link
Member Author

Choose a reason for hiding this comment

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

Making the arguments explicit no longer causes unpleasant wrapping, so I've made the change.

R/mock-oo.R Outdated
if (is.null(def)) {
methods::removeMethod(generic_def, signature, env)
} else {
suppressMessages(methods::setMethod(generic_def, signature, def, env))
Copy link
Member

Choose a reason for hiding this comment

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

We were not suppressing anything before, what are we...uh...suppressing?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's COMPLICATED

#' length(x)
local_mocked_s3_method <- function(
generic,
signature,
Copy link
Member

Choose a reason for hiding this comment

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

I am mildly irked this was called signature and not class, since class is more S3 like

Copy link
Member Author

Choose a reason for hiding this comment

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

TOO LATE

Comment on lines 34 to 48
test_that("can temporarily remove S3 method with NULL", {
x <- structure(list(), class = "test_mock_class2")

local({
local_mocked_s3_method("length", "test_mock_class2", function(x) 42)
expect_length(x, 42)

# Now remove it
local_mocked_s3_method("length", "test_mock_class2", NULL)
expect_length(x, 0)
})

# Method should be removed after scope ends
expect_length(x, 0)
})
Copy link
Member

Choose a reason for hiding this comment

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

What about a test where length.test_mock_class2 does already exists before entering the local(), then you temporarily remove it for the local() scope, and then after exiting the local scope you ensure that the old definition was restored?

Copy link
Member

Choose a reason for hiding this comment

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

it actually looks like you do have a test for this for s4 with mock_age()

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yeah, this test doesn't really focus on the removing. I'll improve.

@hadley hadley merged commit 66ddd0c into main Jan 7, 2026
13 checks passed
@hadley hadley deleted the oo-mocking-enhancement branch January 7, 2026 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

local_mocked_s4_method() doesn't work for generics from other packages

3 participants