Skip to content

Commit

Permalink
Merge pull request #1432 from rstudio/py_slice
Browse files Browse the repository at this point in the history
accept slices in `[`, add `[<-`
  • Loading branch information
t-kalinowski authored Aug 4, 2023
2 parents bd5d0cb + 8b17f46 commit 3453704
Show file tree
Hide file tree
Showing 14 changed files with 381 additions and 158 deletions.
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ S3method(">=",python.builtin.object)
S3method("[",python.builtin.dict)
S3method("[",python.builtin.object)
S3method("[<-",python.builtin.dict)
S3method("[<-",python.builtin.object)
S3method("[[",python.builtin.BaseException)
S3method("[[",python.builtin.dict)
S3method("[[",python.builtin.object)
Expand Down
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
- Updated recommendations in the "python_dependencies" vignette for how R packages
can approach Python dependency management.

- New `[` and `[<-` methods that invoke python `__getitem__`, `__setitem__`
and `__delitem__`. The R generic `[` accepts python-style slice syntax like `x[1:2:3]`.
See examples in `?py_get_item`.

- `virtualenv_create()` gains a `force` argument.

- Fixed an issue where the list of available python versions used by `install_python()`
Expand Down
4 changes: 4 additions & 0 deletions R/RcppExports.R
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@ py_capsule <- function(x) {
.Call(`_reticulate_py_capsule`, x)
}

py_slice <- function(start = NULL, stop = NULL, step = NULL) {
.Call(`_reticulate_py_slice`, start, stop, step)
}

readline <- function(prompt) {
.Call(`_reticulate_readline`, prompt)
}
Expand Down
11 changes: 7 additions & 4 deletions R/python-dict.R
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@
`[[.python.builtin.dict` <- `[.python.builtin.dict`

#' @export
`$<-.python.builtin.dict` <- function(x, name, value) {
if (!py_is_null_xptr(x) && py_available())
py_dict_set_item(x, name, value)
else
`$<-.python.builtin.dict` <- function(x, key, value) {
if (py_is_null_xptr(x) || !py_available())
stop("Unable to assign value (dict reference is NULL)")

if(is.null(value))
py_del_item(x, key)
else
py_dict_set_item(x, key, value)
x
}

Expand Down
195 changes: 195 additions & 0 deletions R/python-item.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@



#' Get/Set/Delete an item from a Python object
#'
#' Access an item from a Python object, similar to how \code{x[key]} might be
#' used in Python code to access an item indexed by `key` on an object `x`. The
#' object's `__getitem__()` `__setitem__()` or `__delitem__()` method will be
#' called.
#'
#' @note The `py_get_item()` always returns an unconverted python object, while
#' `[` will automatically attempt to convert the object if `x` was created
#' with `convert = TRUE`.
#'
#' @param x A Python object.
#' @param key,name,... The key used for item lookup.
#' @param silent Boolean; when \code{TRUE}, attempts to access missing items
#' will return \code{NULL} rather than throw an error.
#' @param value The item value to set. Assigning `value` of `NULL` calls
#' `py_del_item()` and is equivalent to the python expression `del x[key]`. To
#' set an item value of `None`, you can call `py_set_item()` directly, or call
#' `x[key] <- py_none()`
#'
#' @return For `py_get_item()` and `[`, the return value from the
#' `x.__getitem__()` method. For `py_set_item()`, `py_del_item()` and `[<-`,
#' the mutate object `x` is returned.
#'
#' @rdname py_get_item
#' @family item-related APIs
#' @export
#' @examples
#' \dontrun{
#'
#' ## get/set/del item from Python dict
#' x <- r_to_py(list(abc = "xyz"))
#'
#' #' # R expression | Python expression
#' # -------------------- | -----------------
#' x["abc"] # x["abc"]
#' x["abc"] <- "123" # x["abc"] = "123"
#' x["abc"] <- NULL # del x["abc"]
#' x["abc"] <- py_none() # x["abc"] = None
#'
#' ## get item from Python list
#' x <- r_to_py(list("a", "b", "c"))
#' x[0]
#'
#' ## slice a NumPy array
#' x <- np_array(array(1:64, c(4, 4, 4)))
#'
#' # R expression | Python expression
#' # ------------ | -----------------
#' x[0] # x[0]
#' x[, 0] # x[:, 0]
#' x[, , 0] # x[:, :, 0]
#'
#' x[NA:2] # x[:2]
#' x[`:2`] # x[:2]
#'
#' x[2:NA] # x[2:]
#' x[`2:`] # x[2:]
#'
#' x[NA:NA:2] # x[::2]
#' x[`::2`] # x[::2]
#'
#' x[1:3:2] # x[1:3:2]
#' x[`1:3:2`] # x[1:3:2]
#'
#' }
py_get_item <- function(x, key, silent = FALSE) {
ensure_python_initialized()
if (py_is_module_proxy(x))
py_resolve_module_proxy(x)

# NOTE: for backwards compatibility, we make sure to return an R NULL on error
if (silent) {
tryCatch(py_get_item_impl(x, key, FALSE), error = function(e) NULL)
} else {
py_get_item_impl(x, key, FALSE)
}

}

#' @rdname py_get_item
#' @export
py_set_item <- function(x, name, value) {
ensure_python_initialized()
if (py_is_module_proxy(x))
py_resolve_module_proxy(x)
py_set_item_impl(x, name, value)
invisible(x)
}

#' @rdname py_get_item
#' @export
py_del_item <- function(x, name) {
ensure_python_initialized()
if (py_is_module_proxy(x))
py_resolve_module_proxy(x)

if (!py_has_attr(x, "__delitem__"))
stop("Python object has no '__delitem__' method", call. = FALSE)
delitem <- py_to_r(py_get_attr(x, "__delitem__", silent = FALSE))

delitem(name)
invisible(x)
}


#' @rdname py_get_item
#' @export
`[.python.builtin.object` <- function(x, ...) {

key <- dots_to__getitem__key(..., .envir = parent.frame())

out <- if(inherits(key, "python.builtin.tuple"))
py_get_item(x, key)
else
py_get_attr_or_item(x, key, FALSE) # prefer_attr = FALSE
py_maybe_convert(out, py_has_convert(x))
}

#' @rdname py_get_item
#' @export
`[<-.python.builtin.object` <- function(x, ..., value) {
if (py_is_null_xptr(x) || !py_available())
stopf("Unable to assign value (`%s` reference is NULL)", deparse1(substitute(x)))

key <- dots_to__getitem__key(..., .envir = parent.frame())

if(is.null(value))
py_del_item(x, key)
else
py_set_item(x, key, value)
}


dots_to__getitem__key <- function(..., .envir) {
dots <- lapply(eval(substitute(alist(...))), function(d) {

if(is_missing(d))
return(py_slice())

if (is_has_colon(d)) {

if (is_colon_call(d)) {

d <- as.list(d)[-1L]

if (is_colon_call(d[[1L]] -> d1)) # step supplied
d <- c(as.list(d1)[-1L], d[-1L])

} else { # single name with colon , like `::2`

d <- deparse(d, width.cutoff = 500L, backtick = FALSE)
d <- strsplit(d, ":", fixed = TRUE)[[1L]]
d[!nzchar(d)] <- "NULL"
d <- lapply(d, parse1) # rlang::parse_expr
}

if(!length(d) %in% 1:3)
stop("Only 1, 2, or 3 arguments can be supplied as a python slice")

d <- lapply(d, eval, envir = .envir)
d <- lapply(d, function(e) if(identical(e, NA) ||
identical(e, NA_integer_) ||
identical(e, NA_real_)) NULL else e)

return(do.call(py_slice, d))
}

# else, eval normally
d <- eval(d, envir = .envir)
if(rlang::is_scalar_integerish(d))
d <- as.integer(d)
d
})

if(length(dots) == 1L)
dots[[1L]]
else
tuple(dots)
}

# TODO: update these to use rlang
is_has_colon <- function(x)
is_colon_call(x) || (is.symbol(x) && grepl(":", as.character(x), fixed = TRUE))

is_colon_call <- function(x)
is.call(x) && identical(x[[1L]], quote(`:`))

is_missing <- function(x) identical(x, quote(expr =))

parse1 <- function (text) parse(text = text, keep.source = FALSE)[[1L]]

84 changes: 0 additions & 84 deletions R/python.R
Original file line number Diff line number Diff line change
Expand Up @@ -406,19 +406,12 @@ py_get_attr_or_item <- function(x, name, prefer_attr) {
`$.python.builtin.object` <- function(x, name) {
py_get_attr_or_item(x, name, TRUE)
}

#' @export
`[.python.builtin.object` <- function(x, name) {
py_get_attr_or_item(x, name, FALSE)
}

#' @export
`[[.python.builtin.object` <- function(x, name) {
py_get_attr_or_item(x, name, FALSE)
}



# the as.environment generic enables pytyhon objects that manifest
# as R functions (e.g. for functions, classes, callables, etc.) to
# be automatically converted to enviroments during the construction
Expand Down Expand Up @@ -1039,83 +1032,6 @@ py_get_attr_types <- function(x,
py_get_attr_types_impl(x, names, resolve_properties)
}

#' Get an item from a Python object
#'
#' Retrieve an item from a Python object, similar to how
#' \code{x[name]} might be used in Python code to access an
#' item indexed by `key` on an object `x`. The object's
#' `__getitem__` method will be called.
#'
#' @param x A Python object.
#' @param key The key used for item lookup.
#' @param silent Boolean; when \code{TRUE}, attempts to access
#' missing items will return \code{NULL} rather than
#' throw an error.
#'
#' @family item-related APIs
#' @export
py_get_item <- function(x, key, silent = FALSE) {
ensure_python_initialized()
if (py_is_module_proxy(x))
py_resolve_module_proxy(x)

# NOTE: for backwards compatibility, we make sure to return an R NULL on error
if (silent) {
tryCatch(py_get_item_impl(x, key, FALSE), error = function(e) NULL)
} else {
py_get_item_impl(x, key, FALSE)
}

}

#' Set an item for a Python object
#'
#' Set an item on a Python object, similar to how
#' \code{x[name] = value} might be used in Python code to
#' set an item called `name` with value `value` on object
#' `x`. The object's `__setitem__` method will be called.
#'
#' @param x A Python object.
#' @param name The item name.
#' @param value The item value.
#'
#' @return The (mutated) object `x`, invisibly.
#'
#' @family item-related APIs
#' @export
py_set_item <- function(x, name, value) {
ensure_python_initialized()
if (py_is_module_proxy(x))
py_resolve_module_proxy(x)
py_set_item_impl(x, name, value)
invisible(x)
}

#' Delete / remove an item from a Python object
#'
#' Delete an item associated with a Python object, as
#' through its `__delitem__` method.
#'
#' @param x A Python object.
#' @param name The item name.
#'
#' @return The (mutated) object `x`, invisibly.
#'
#' @family item-related APIs
#' @export
py_del_item <- function(x, name) {
ensure_python_initialized()
if (py_is_module_proxy(x))
py_resolve_module_proxy(x)

if (!py_has_attr(x, "__delitem__"))
stop("Python object has no '__delitem__' method", call. = FALSE)
delitem <- py_to_r(py_get_attr(x, "__delitem__", silent = FALSE))

delitem(name)
invisible(x)
}




Expand Down
26 changes: 0 additions & 26 deletions man/py_del_item.Rd

This file was deleted.

Loading

0 comments on commit 3453704

Please sign in to comment.