-
Notifications
You must be signed in to change notification settings - Fork 339
Enhance OO mocking #2303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enhance OO mocking #2303
Conversation
Fixes #2302
|
Claude finished @hadley's task —— View job PR Review: Enhance OO mockingReviewed the OO mocking enhancements. Found one potential issue: Issue in 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 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. |
R/mock-oo.R
Outdated
| check_function(definition) | ||
| check_function(definition, allow_null = TRUE) | ||
|
|
||
| generic_def <- get(generic, envir = frame) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused. Remove?
| genfun <- get(generic, envir = envir) | ||
| s3_envir <- environment(genfun) %||% .BaseNamespaceEnv |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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) | ||
|
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
| 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TOO LATE
| 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) | ||
| }) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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.
Fixes #2302