Few issues solved - separator/DRY/list(...)#96
Conversation
R/appenders.R
Outdated
| suppressWarnings(R.utils::countLines(file)), | ||
| error = function(e) 0) | ||
| n_bytes <- ifelse(file.exists(file), file.info(file)$size, 0) | ||
| n_bytes <- if (file.exists(file)) file.info(file)$size else 0 |
There was a problem hiding this comment.
what's the advantage of calling if/else (without the curly braces) compared to ifelse?
There was a problem hiding this comment.
It is more secure and readable, when working with scalars.
Here a Hadley Wickham opinion who is in favour of such step.
https://twitter.com/hadleywickham/status/928983687605059585
There was a problem hiding this comment.
Strongly agree with @Polkas . Note also this edge case (not relevant for file.exists(file), since it can not return NA, but still):
> ifelse(NA, TRUE, FALSE)
[1] NA
> `if`(NA, TRUE, FALSE)
Error in if (NA) TRUE else FALSE : missing value where TRUE/FALSE needed
Note also that for simple conditionals, I tend to use backticked if to keep the functional style.
There was a problem hiding this comment.
Hm, got it, thanks for the pointers. Although makes sense from a perspective, but I have an aversion to using if without the curly braces, as have been hurt by how the parser handles it (single VS multiple statements), e.g. with covr false positives or missed else branches on multiline expressions.
Although I've not run into this issue recently, habits die hard :)
And the R manual of if still suggests always keeping the braces .. which makes it ugly for single-line statements.
Anyway, I will keep thinking about it, and looking at the rest of the PR in the meanwhile -- thanks again!!
There was a problem hiding this comment.
I understand your doubts, so what about ifelse(isTRUE(cond), ..., ...).
There was a problem hiding this comment.
Sorry, let's not wrap file.exists in isTRUE 🤐
I don't think there's any benefit in that extra check, still makes the code more complex and a bit slower.
So far, the backtick + if combo is the most appealing to me .. but still feels like black magic, so probably we need something easier to grasp -- which might be just the ugly multi-line if conditionals?
Anyway, this is more of a language syntax question, and I feel like we just miss a nice R implementation of the ternary operator, which is unlikely that we will resolve in this thread :)
So I might be open to updating the above to avoid if conditionals altogether with something like:
na.omit(c(file.info(file)$size, 0))[1]But let me rather check the rest of the PR instead at last :)
R/utils.R
Outdated
| res <- capture.output(log_level(level = level, namespace = namespace), type = "message") | ||
| if (length(res) == 0) { | ||
| # catch output | ||
| res <- capture.output(log_level(level = level, namespace = namespace), type = "output") | ||
| } |
There was a problem hiding this comment.
do we need to check both stderr and stdout? setting appender_console (which is an alias to appender_stderr) should (in theory) write to the former, and nothing to the latter.
There was a problem hiding this comment.
I added the log_appender(appender_console) call as I got double prints in some tests. Preciously for helpers testing where there is used log_appender(appender_stdout). catch_base_log should be neutral for the rest of the session, that is why I am doing stderr redirection (2>).
There was a problem hiding this comment.
double prints should not happen, except if namespaces and/or indexes are messed up. I think passing namespace in the other related comment, and systematically everywhere in this function might resolve it?
There was a problem hiding this comment.
You are right namespaces were a clue. More than that I had to use fallback_namespace.
#' catch_base_log(INFO, NA_character_)
#' logger <- layout_glue_generator(format = '{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}')
#' log_layout(logger)
#' catch_base_log(INFO, NA_character_)
#' fun <- function() catch_base_log(INFO, NA_character_)
#' fun()
#' catch_base_log(INFO, NA_character_, .topcall = call("funLONG"))
#' }
catch_base_log <- function(
level,
namespace,
.topcall = sys.call(-1),
.topenv = parent.frame()
) {
namespace <- fallback_namespace(namespace)
orginal_appender <- log_appender(namespace = namespace)
log_appender(appender_console, namespace = namespace)
# catch error, warning or message
res <- capture.output(
log_level(
level = level,
namespace = namespace,
.topcall = .topcall,
.topenv = .topenv
),
type = "message"
)
log_appender(orginal_appender, namespace = namespace)
res
}- getting namespace and if not exists then return global.
- getting current namespace appender, to be able to return to it later.
- temporally setting appender_console (stderr) inside the namespace and catch (run) capture.output(log_layout(level, namespace = namespace), type = "message"). log_layout will not print to the console as we capture an expected stderr.
- Go back to the original appender and apply it to the namespace
- return the catched header (from log_layout), we could e.g. count its characters for log_separator
We get a log_layout header and do not influence the environment
There was a problem hiding this comment.
Please read on github as I added small edit.
There was a problem hiding this comment.
In the one of commits I added testthat tests for the catch_base_log function.
There was a problem hiding this comment.
I start to consider .topcall so the function is even more secure when the layout depends on where the function is called. I added .topcall argument to log_separator too. More tests are added for these scenerios.
|
sorry for the delay with this - got distracted by #98 and the weekend .. I will pick it up again in a few days |
Co-authored-by: Gergely Daróczi <daroczig@rapporter.net>
|
This is great, thank you very much again! Also thanks for the ping in the other PR on the Travis -> GH actions update 👍 I am ready to merge this, but may I ask the below changes before that?
No problem if not, I can totally do that, just let me know. |
|
I reverted ifelse, " quote and roxygen2 long lines split. Moreover I go through new code and update it to use 4 spaces. Have a great new year:) |
tests/testthat/test-utils.R
Outdated
| expect_true(nchar(logger:::catch_base_log(INFO, NA_character_)) == 0) | ||
| log_layout(layout_original) | ||
| layout_original <- log_layout(namespace = 'TEMP') | ||
| logger <- layout_glue_generator(format = '{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}') |
There was a problem hiding this comment.
I suspect the PR builder checks (tests) are failing due to this .. nod and pid can have variable with, maybe test without those?
There was a problem hiding this comment.
There was a problem hiding this comment.
I change the computer and now it works on both of them. You are right node and pid have to be removed from tests as are machine specific. Good finding, you save a lot of my time:) Of course now it looks to be obvious.
There was a problem hiding this comment.
ah looks like default time has different format on old Linux machine. I will remove it from layout and we should be at home. Rest of machines are ok.
Codecov Report
@@ Coverage Diff @@
## master #96 +/- ##
==========================================
+ Coverage 79.11% 80.76% +1.65%
==========================================
Files 10 10
Lines 522 520 -2
==========================================
+ Hits 413 420 +7
+ Misses 109 100 -9
Continue to review full report at Codecov.
|
|
Luckily I have a pythonanywhere account, with a Linux and the old R version (3.6.3). Looks like we find some edge case. should be "TEMP/nchar INFO: " and we get "TEMP/eval INFO: " (.topcall == eval(expr, pf) not nchar(logger:::catch_base_log(...))) |
|
I am pretty sure that is due to One workaround, just like above, is making the call happen outside of |
|
Finally I removed one line (one test), as I think we should still test how |
daroczig
left a comment
There was a problem hiding this comment.
Thanks a lot again for this, and sorry for the back-and-forths 😅 This was a great idea and excellent contribution 🙇

I think it is enough for this PR, new ideas I will push in another one.
After many tests I think that the new utils function
catch_base_logis the best way to get log header length (nchar). It might return wrong number of chars if log header has different length under different environments.Another idea which was not implemented is to add
widthargument to logger -> log_level. Thus control message/output length at the creation level.I provide informative roxygen2 docs when log_separator is not perfectly supported.
list(...)#92 For list(..) I transfer part of the code.log_baseifelseis recommended for vectors where as for single logical we should useif else