File: capt.R

package info (click to toggle)
r-cran-diffobj 0.3.5-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 2,432 kB
  • sloc: ansic: 455; javascript: 96; sh: 32; makefile: 8
file content (420 lines) | stat: -rwxr-xr-x 14,025 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# Copyright (C) 2021 Brodie Gaslam
#
# This file is part of "diffobj - Diffs for R Objects"
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# Go to <https://www.r-project.org/Licenses/GPL-2> for a copy of the license.

# Capture output of print/show/str; unfortunately doesn't have superb handling
# of errors during print/show call, though hopefully these are rare
#
# x is a quoted call to evaluate

capture <- function(x, etc, err) {
  capt.width <- etc@text.width
  if(capt.width) {
    opt.set <- try(width.old <- options(width=capt.width), silent=TRUE)
    if(inherits(opt.set, "try-error")) {
      warning(
        "Unable to set desired width ", capt.width, ", (",
        conditionMessage(attr(opt.set, "condition")), ");",
        "proceeding with existing setting."
      )
    } else on.exit(options(width.old))
  }
  # Note, we use `tempfile` for capture as that appears much faster than normal
  # capture without a file

  capt.file <- tempfile()
  on.exit(unlink(capt.file), add=TRUE)
  res <- try({
    capture.output(eval(x, etc@frame), file=capt.file)
    obj.out <- readLines(capt.file)
  })
  if(inherits(res, "try-error"))
    err(
      "Failed attempting to get text representation of object: ",
      conditionMessage(attr(res, "condition"))
    )
  html_ent_sub(res, etc@style)
}
# capture normal prints, along with default prints to make sure that if we
# do try to wrap an atomic vector print it is very likely to be in a format
# we are familiar with and not affected by a non-default print method

capt_print <- function(target, current, etc, err, extra){
  dots <- extra
  # What about S4?
  if(getRversion() >= "3.2.0") {
    print.match <- try(
      match.call(
        get("print", envir=etc@frame, mode='function'),
        as.call(c(list(quote(print), x=NULL), dots)),
        envir=etc@frame
    ) )
  } else {
    # this may be sub-optimal, but match.call does not support the envir arg
    # prior to this
    # nocov start
    print.match <- try(
      match.call(
        get("print", envir=etc@frame),
        as.call(c(list(quote(print), x=NULL), dots))
    ) )
    # nocov end
  }
  if(inherits(print.match, "try-error"))
    err("Unable to compose `print` call")

  names(print.match)[[2L]] <- ""
  tar.call <- cur.call <- print.match

  if(length(dots)) {
    if(!is.null(etc@tar.exp)) tar.call[[2L]] <- etc@tar.exp
    if(!is.null(etc@cur.exp)) cur.call[[2L]] <- etc@cur.exp
    etc@tar.banner <- deparse(tar.call)[[1L]]
    etc@cur.banner <- deparse(cur.call)[[1L]]
  }
  tar.call.q <- if(is.call(target) || is.symbol(target))
    call("quote", target) else target
  cur.call.q <- if(is.call(current) || is.symbol(current))
    call("quote", current) else current

  if(!is.null(target)) tar.call[[2L]] <- tar.call.q
  if(!is.null(current)) cur.call[[2L]] <- cur.call.q

  # If dimensioned object, and in auto-mode, switch to side by side if stuff is
  # narrow enough to fit

  if((!is.null(dim(target)) || !is.null(dim(current)))) {
    cur.capt <- capture(cur.call, etc, err)
    tar.capt <- capture(tar.call, etc, err)
    etc <- set_mode(etc, tar.capt, cur.capt)
  } else {
    etc <- if(etc@mode == "auto") sideBySide(etc) else etc
    cur.capt <- capture(cur.call, etc, err)
    tar.capt <- capture(tar.call, etc, err)
  }
  if(isTRUE(etc@guides)) etc@guides <- guidesPrint
  if(isTRUE(etc@trim)) etc@trim <- trimPrint

  diff.out <- line_diff(target, current, tar.capt, cur.capt, etc=etc, warn=TRUE)
  diff.out@capt.mode <- "print"
  diff.out
}
# Tries various different `str` settings to get the best possible output

capt_str <- function(target, current, etc, err, extra){
  # Match original call and managed dots, in particular wrt to the
  # `max.level` arg
  dots <- extra
  frame <- etc@frame
  line.limit <- etc@line.limit
  if("object" %in% names(dots))
    err("You may not specify `object` as part of `extra`")

  if(getRversion() < "3.2.0") {
    # nocov start
    str.match <- match.call(
      str_tpl,
      call=as.call(c(list(quote(str), object=NULL), dots))
    )
    # nocov end
  } else {
    str.match <- match.call(
      str_tpl,
      call=as.call(c(list(quote(str), object=NULL), dots)), envir=etc@frame
    )
  }
  names(str.match)[[2L]] <- ""

  # Handle auto mode (side by side always for `str`)

  if(etc@mode == "auto") etc <- sideBySide(etc)

  # Utility function; defining in body so it has access to `err`

  eval_try <- function(match.list, index, envir)
    tryCatch(
      eval(match.list[[index]], envir=envir),
      error=function(e)
        err("Error evaluating `", index, "` arg: ", conditionMessage(e))
    )
  # Setup / process extra args

  auto.mode <- FALSE
  max.level.supplied <- FALSE
  if(
    max.level.pos <- match("max.level", names(str.match), nomatch=0L)
  ) {
    # max.level specified in call; check for special 'auto' case
    max.level.eval <- eval_try(str.match, "max.level", etc@frame)
    if(identical(max.level.eval, "auto")) {
      auto.mode <- TRUE
      str.match[["max.level"]] <- NA
    } else {
      max.level.supplied <- TRUE
    }
  } else {
    str.match[["max.level"]] <- NA
    auto.mode <- TRUE
    max.level.pos <- length(str.match)
    max.level.supplied <- FALSE
  }
  # Was wrap specified in strict width mode?  Not sure this is correct any more;
  # should probably be looking at extra args.

  wrap <- FALSE
  if("strict.width" %in% names(str.match)) {
    res <- eval_try(str.match, "strict.width", etc@frame)
    wrap <- is.character(res) && length(res) == 1L && !is.na(res) &&
      nzchar(res) && identical(res, substr("wrap", 1L, nchar(res)))
  }
  if(auto.mode) {
    msg <-
      "Specifying `%s` may cause `str` output level folding to be incorrect"
    if("comp.str" %in% names(str.match)) warning(sprintf(msg, "comp.str"))
    if("indent.str" %in% names(str.match)) warning(sprintf(msg, "indent.str"))
  }
  # don't want to evaluate target and current more than once, so can't eval
  # tar.exp/cur.exp, so instead run call with actual object

  tar.call <- cur.call <- str.match

  tar.call.q <- if(is.call(target) || is.symbol(target))
    call("quote", target) else target
  cur.call.q <- if(is.call(current) || is.symbol(current))
    call("quote", current) else current

  if(!is.null(target)) tar.call[[2L]] <- tar.call.q
  if(!is.null(current)) cur.call[[2L]] <- cur.call.q

  # Run str

  capt.width <- etc@text.width
  has.diff <- has.diff.prev <- FALSE

  # we used to strip_hz_control here, but shouldn't have to since handled by
  # line_diff

  tar.capt <- capture(tar.call, etc, err)
  tar.lvls <- str_levels(tar.capt, wrap=wrap)
  cur.capt <- capture(cur.call, etc, err)
  cur.lvls <- str_levels(cur.capt, wrap=wrap)

  prev.lvl.hi <- lvl <- max.depth <- max(tar.lvls, cur.lvls)
  prev.lvl.lo <- 0L
  first.loop <- TRUE
  safety <- 0L
  warn <- TRUE

  if(isTRUE(etc@guides)) etc@guides <- guidesStr
  if(isTRUE(etc@trim)) etc@trim <- trimStr

  tar.str <- tar.capt
  cur.str <- cur.capt

  diff.obj <- diff.obj.full <- line_diff(
    target, current, tar.str, cur.str, etc=etc, warn=warn
  )
  if(!max.level.supplied) {
    repeat{
      if((safety <- safety + 1L) > max.depth && !first.loop)
        # nocov start
        stop(
          "Logic Error: exceeded list depth when comparing structures; contact ",
          "maintainer."
        )
        # nocov end
      if(!first.loop) {
        tar.str <- tar.capt[tar.lvls <= lvl]
        cur.str <- cur.capt[cur.lvls <= lvl]

        diff.obj <- line_diff(
          target, current, tar.str, cur.str, etc=etc, warn=warn
        )
      }
      if(diff.obj@hit.diffs.max) warn <- FALSE
      has.diff <- suppressWarnings(any(diff.obj))

      # If there are no differences reducing levels isn't going to help to
      # find one; additionally, if not in auto.mode we should not be going
      # through this process

      if(first.loop && !has.diff) break
      first.loop <- FALSE

      if(line.limit[[1L]] < 1L) break

      line.len <- diff_line_len(
        diff.obj@diffs, etc=etc, tar.capt=tar.str, cur.capt=cur.str
      )
      # We need a higher level if we don't have diffs

      if(!has.diff && prev.lvl.hi - lvl > 1L) {
        prev.lvl.lo <- lvl
        lvl <- lvl + as.integer((prev.lvl.hi - lvl) / 2)
        tar.call[[max.level.pos]] <- lvl
        cur.call[[max.level.pos]] <- lvl
        next
      } else if(!has.diff) {
        diff.obj <- diff.obj.full
        lvl <- NULL
        break
      }
      # If we have diffs, need to check whether we should try to reduce lines
      # to get under line limit

      if(line.len <= line.limit[[1L]]) {
        # We fit, nothing else to do
        break
      }
      if(lvl - prev.lvl.lo > 1L) {
        prev.lvl.hi <- lvl
        lvl <- lvl - as.integer((lvl - prev.lvl.lo) / 2)
        tar.call[[max.level.pos]] <- lvl
        cur.call[[max.level.pos]] <- lvl
        next
      }
      # Couldn't get under limit, so use first run results

      diff.obj <- diff.obj.full
      lvl <- NULL
      break
    }
  } else {
    tar.str <- tar.capt[tar.lvls <= max.level.eval]
    cur.str <- cur.capt[cur.lvls <= max.level.eval]

    lvl <- max.level.eval
    diff.obj <- line_diff(target, current, tar.str, cur.str, etc=etc, warn=warn)
  }
  if(auto.mode && !is.null(lvl) && lvl < max.depth) {
    str.match[[max.level.pos]] <- lvl
  } else if (!max.level.supplied || is.null(lvl)) {
    str.match[[max.level.pos]] <- NULL
  }
  tar.call <- cur.call <- str.match
  if(!is.null(etc@tar.exp)) tar.call[[2L]] <- etc@tar.exp
  if(!is.null(etc@cur.exp)) cur.call[[2L]] <- etc@cur.exp
  if(is.null(etc@tar.banner))
    diff.obj@etc@tar.banner <- deparse(tar.call)[[1L]]
  if(is.null(etc@cur.banner))
    diff.obj@etc@cur.banner <- deparse(cur.call)[[1L]]

  # Track total differences in fully expanded view so we can report hidden
  # diffs when folding levels

  diff.obj@diff.count.full <- count_diffs(diff.obj.full@diffs)
  diff.obj@capt.mode <- "str"
  diff.obj
}
capt_chr <- function(target, current, etc, err, extra){
  tar.capt <- if(!is.character(target))
    do.call(as.character, c(list(target), extra), quote=TRUE) else target
  cur.capt <- if(!is.character(current))
    do.call(as.character, c(list(current), extra), quote=TRUE) else current

  # technically possible to have a character method that doesn't return a
  # character object...

  if((tt <- typeof(tar.capt)) != 'character')
    stop("Coercion of `target` did not produce character object (", tt, ").")
  if((tc <- typeof(cur.capt)) != 'character')
    stop("Coercion of `current` did not produce character object (", tc, ").")

  # drop attributes

  tar.capt <- c(tar.capt)
  cur.capt <- c(cur.capt)

  if(anyNA(tar.capt)) tar.capt[is.na(tar.capt)] <- "NA"
  if(anyNA(cur.capt)) cur.capt[is.na(cur.capt)] <- "NA"

  etc <- set_mode(etc, tar.capt, cur.capt)
  if(isTRUE(etc@guides)) etc@guides <- guidesChr
  if(isTRUE(etc@trim)) etc@trim <- trimChr

  diff.out <- line_diff(
    target, current, html_ent_sub(tar.capt, etc@style),
    html_ent_sub(cur.capt, etc@style), etc=etc
  )
  diff.out@capt.mode <- "chr"
  diff.out
}
capt_deparse <- function(target, current, etc, err, extra){
  dep.try <- try({
    tar.capt <- do.call(deparse, c(list(target), extra), quote=TRUE)
    cur.capt <- do.call(deparse, c(list(current), extra), quote=TRUE)
  })
  if(inherits(dep.try, "try-error"))
    err("Error attempting to deparse object(s)")

  etc <- set_mode(etc, tar.capt, cur.capt)
  if(isTRUE(etc@guides)) etc@guides <- guidesDeparse
  if(isTRUE(etc@trim)) etc@trim <- trimDeparse

  diff.out <- line_diff(
    target, current, html_ent_sub(tar.capt, etc@style),
    html_ent_sub(cur.capt, etc@style), etc=etc
  )
  diff.out@capt.mode <- "deparse"
  diff.out
}
capt_file <- function(target, current, etc, err, extra) {
  tar.capt <- try(do.call(readLines, c(list(target), extra), quote=TRUE))
  if(inherits(tar.capt, "try-error")) err("Unable to read `target` file.")
  cur.capt <- try(do.call(readLines, c(list(current), extra), quote=TRUE))
  if(inherits(cur.capt, "try-error")) err("Unable to read `current` file.")

  etc <- set_mode(etc, tar.capt, cur.capt)
  if(isTRUE(etc@guides)) etc@guides <- guidesFile
  if(isTRUE(etc@trim)) etc@trim <- trimFile

  diff.out <- line_diff(
    tar.capt, cur.capt, html_ent_sub(tar.capt, etc@style),
    html_ent_sub(cur.capt, etc@style), etc=etc
  )
  diff.out@capt.mode <- "file"
  diff.out
}
capt_csv <- function(target, current, etc, err, extra){
  tar.df <- try(do.call(read.csv, c(list(target), extra), quote=TRUE))
  if(inherits(tar.df, "try-error")) err("Unable to read `target` file.")
  if(!is.data.frame(tar.df))
    err("`target` file did not produce a data frame when read")   # nocov
  cur.df <- try(do.call(read.csv, c(list(current), extra), quote=TRUE))
  if(inherits(cur.df, "try-error")) err("Unable to read `current` file.")
  if(!is.data.frame(cur.df))
    err("`current` file did not produce a data frame when read")  # nocov

  capt_print(tar.df, cur.df, etc, err, extra)
}
# Sets mode to "unified" if stuff is too wide to fit side by side without
# wrapping otherwise sets it in "sidebyside"

set_mode <- function(etc, tar.capt, cur.capt) {
  stopifnot(is(etc, "Settings"), is.character(tar.capt), is.character(cur.capt))
  if(etc@mode == "auto") {
    if(
      any(
        nchar2(cur.capt, sgr.supported=etc@sgr.supported) > etc@text.width.half
      ) ||
      any(
        nchar2(tar.capt, sgr.supported=etc@sgr.supported) > etc@text.width.half
      )
    ) {
      etc@mode <- "unified"
  } }
  if(etc@mode == "auto") etc <- sideBySide(etc)
  etc
}