findpython/0000755000176200001440000000000014717032436012443 5ustar liggesusersfindpython/tests/0000755000176200001440000000000013400041556013574 5ustar liggesusersfindpython/tests/run-all.R0000644000176200001440000000005612301703766015301 0ustar liggesuserslibrary("testthat") test_check("findpython") findpython/tests/testthat/0000755000176200001440000000000014716702470015446 5ustar liggesusersfindpython/tests/testthat/test-findpython.r0000644000176200001440000000503614716702470020774 0ustar liggesusers# Copyright (c) 2024 Trevor L. Davis # Copyright (c) 2014 Paul Gilbert # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. test_that("Python works as expected", { cmd_exists <- can_find_python_cmd() skip_if_not(as.logical(cmd_exists), "Can't find any version of python") cmd <- attr(cmd_exists, "python_cmd") expect_true(can_find_python_cmd(required_modules = "sys")) expect_true(is_python_sufficient(cmd, required_modules = "sys")) expect_false(can_find_python_cmd(required_modules = "xxxyyyzzz", silent = TRUE)) expect_false(is_python_sufficient(cmd, required_modules = "xxxyyyzzz")) }) test_that("Python 2 works as expected", { has_python2 <- can_find_python_cmd( minimum_version = "2.0", maximum_version = "2.7", silent = TRUE ) skip_if_not(as.logical(has_python2), "Can't find Python 2") cmd <- attr(has_python2, "python_cmd") expect_true(is_python_sufficient(cmd, required_modules = "sys")) expect_false(is_python_sufficient(cmd, required_modules = "xxxyyyzzz")) }) test_that("Python 3 works as expected", { has_python3 <- can_find_python_cmd( minimum_version = "3.0", silent = TRUE ) skip_if_not(as.logical(has_python3), "Can't find Python 3") cmd <- attr(has_python3, "python_cmd") expect_true(is_python_sufficient(cmd, required_modules = "sys")) expect_true(is_python_sufficient(cmd, required_modules = "json | simplejson")) expect_false(is_python_sufficient(cmd, required_modules = "xxxyyyzzz")) }) findpython/MD50000644000176200001440000000107114717032436012752 0ustar liggesusers6d8337abf0b0c6efd8021d4ebfee555d *DESCRIPTION 605d929013717e099702d27414b41936 *LICENSE 2ac170dcd095e1526fdf4d56b295cc9d *NAMESPACE f6b8dd1f895cfb60a26eca02bc2b42b0 *NEWS.md 94df5c7955fdca5708a779d0c2f6a9e6 *R/find_python_cmd.r dab61324df5bcf64dddc0c70b900ed8c *README.md 8b267b940982f68506d72866554b6ec7 *man/can_find_python_cmd.Rd 175ad9105a059558b02ab682338359bb *man/find_python_cmd.Rd 288908d8f923c4271d0e8faab770debb *man/is_python_sufficient.Rd e9f668748d07cefc493a1a111d403784 *tests/run-all.R 8157425df54b9a711ce2bd9d22ccc959 *tests/testthat/test-findpython.r findpython/R/0000755000176200001440000000000014716702470012645 5ustar liggesusersfindpython/R/find_python_cmd.r0000644000176200001440000002350514716702470016201 0ustar liggesusers# Copyright (c) 2024 Trevor L. Davis # Copyright (c) 2014 Paul Gilbert # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. #' Tests whether the python command is sufficient #' #' `is_python_sufficient()` checks whether a given python binary has all the #' desired features (minimum and/or maximum version number and/or access to #' certain modules). #' #' @param path The path to a given python binary. #' If binary is on system path just the binary name will work. #' @param minimum_version The minimum version of python it should be. #' Should be a string with major and minor number separated by a `.`. #' If left NULL won't impose such a restriction. #' @param maximum_version The maximum version of python it should be. #' Should be a string with major and minor number separated by a `.`. #' If left NULL won't impose such a restriction. #' @param required_modules Which modules should be required. #' Can use a single `|` to represent a single either-or requirement like `"json|simplejson"`. #' If left `NULL` won't impose such a restriction. #' @return `TRUE` or `FALSE` depending on whether the python binary met all requirements #' @examples #' try({ #' cmd <- find_python_cmd() #' is_python_sufficient(cmd, minimum_version = "3.3", required_modules = "sys") #' }) #' @export is_python_sufficient <- function(path, minimum_version=NULL, maximum_version=NULL, required_modules=NULL) { path <- normalizePath(path, mustWork = FALSE) python_code <- vector("character") if (!is.null(required_modules)) { import_code <- create_import_code(required_modules) python_code <- append(python_code, import_code) } version_code <- create_version_checking_code(minimum_version, maximum_version) python_code <- append(python_code, version_code) ok_message <- "Everything worked out okay" python_code <- append(python_code, paste("print(", shQuote(ok_message), ")", sep = "")) tryCatch({ output <- system2(path, input = python_code, stdout = TRUE, stderr = FALSE) any(grepl(ok_message, output)) }, warning = function(w) { FALSE }, error = function(e) { FALSE }) } #' Find a suitable python cmd or give error if not possible #' #' `find_python_cmd()` finds a suitable python cmd or raises an error if not possible #' #' @inheritParams is_python_sufficient #' @param error_message What error message the user will see if couldn't find a sufficient python binary. #' If left NULL will print out a default message. #' @return The path to an appropriate python binary. If such a path wasn't found then it will throw an error. #' @examples #' try(find_python_cmd()) #' try(find_python_cmd(minimum_version = "2.6", maximum_version = "2.7")) #' try(find_python_cmd(required_modules = c("argparse", "json | simplejson"))) #' @seealso [can_find_python_cmd()] for a wrapper which doesn't throw an error #' @export find_python_cmd <- function(minimum_version = NULL, maximum_version = NULL, required_modules = NULL, error_message = NULL) { python_cmds <- get_python_cmds() for (cmd in python_cmds) { if (is_python_sufficient(cmd, minimum_version, maximum_version, required_modules)) { return(cmd) } } # Try using reticulate::py_discover_config()$python_versions if can't find a suitable command if (requireNamespace("reticulate", quietly = TRUE)) { python_cmds <- reticulate::py_discover_config()$python_versions for (cmd in python_cmds) { if (is_python_sufficient(cmd, minimum_version, maximum_version, required_modules)) { return(cmd) } } } if (is.null(error_message)) { error_message <- default_error_message(minimum_version, maximum_version, required_modules) } stop(error_message) } default_error_message <- function(minimum_version = NULL, maximum_version = NULL, required_modules = NULL) { paste("Couldn't find a sufficient Python binary.", "If you haven't installed the Python dependency yet please do so.", "If you have but it isn't on the system path (as is default on Windows) please add it to path", "or set options('python_cmd'='/path/to/binary') ", "or set the PYTHON, PYTHON2, or PYTHON3 environmental variables.", if (!is.null(minimum_version)) paste("Python must be at least version", minimum_version), if (!is.null(maximum_version)) paste("Python must be at most version", maximum_version), if (!is.null(required_modules)) paste("Python must have access to the modules:", paste(required_modules, collapse = ", "))) } get_python_cmds <- function() { python_cmds <- c(getOption("python_cmd", ""), "python", Sys.getenv("PYTHON", ""), paste0("python4.", seq(9, 0, by = -1)), Sys.getenv("PYTHON4", ""), "python4", paste0("python3.", seq(20, 0, by = -1)), Sys.getenv("PYTHON3", ""), "python3", paste0("python2.", seq(7, 0, by = -1)), Sys.getenv("PYTHON2", ""), "python2", "pypy", sprintf("C:/Python%s/python", c(49:20))) python_cmds <- normalizePath(python_cmds, mustWork = FALSE) python_cmds <- unique(python_cmds) python_cmds <- Sys.which(python_cmds) python_cmds[which(python_cmds != "")] } #' Determines whether or not it can find a suitable python cmd #' #' `can_find_python_cmd()` runs [find_python_cmd()] and returns whether it could find a suitable python cmd. #' If it was successful its output also saves the found command as an attribute. #' #' @inheritParams find_python_cmd #' @param silent Passed to `try`, whether any error messages from [find_python_cmd()] should be suppressed #' @return `TRUE` or `FALSE` depending on whether #' [find_python_cmd()] could find an appropriate python binary. #' If `TRUE` the path to an appropriate python binary is also set as an attribute. #' @examples #' did_find_cmd <- can_find_python_cmd() #' python_cmd <- attr(did_find_cmd, "python_cmd") #' @seealso [find_python_cmd()] #' @export can_find_python_cmd <- function(minimum_version = NULL, maximum_version = NULL, required_modules = NULL, error_message = NULL, silent = FALSE) { python_cmd <- try(find_python_cmd(minimum_version = minimum_version, maximum_version = maximum_version, required_modules = required_modules, error_message = error_message), silent = silent) if (inherits(python_cmd, "try-error")) { r <- FALSE } else { r <- TRUE attr(r, "python_cmd") <- python_cmd } r } # Create appropriate module import code create_import_code <- function(required_modules) { import_code <- vector("character") for (module in required_modules) { if (grepl("\\|", module)) { module <- gsub(" ", "", module) module <- strsplit(module, "\\|")[[1]] import_code <- append(import_code, c("try:", paste(" import", module[1]), "except ImportError:", paste(" import", module[2]))) } else { import_code <- append(import_code, paste("import", module)) } } import_code } # Create appropriate version checking code create_version_checking_code <- function(minimum_version = NULL, maximum_version = NULL) { if (is.null(minimum_version) && is.null(maximum_version)) { return(c()) } else { version_code <- c("import sys") } if (!is.null(minimum_version)) { min_version <- strsplit(minimum_version, "\\.")[[1]] major <- min_version[1] minor <- min_version[2] version_code <- append(version_code, c(paste("if sys.version_info.major <", major, ": raise Exception('Major version too low')"), paste("if sys.version_info.major ==", major, "and sys.version_info.minor <", minor, ": raise Exception('Minor version too low')"))) } if (!is.null(maximum_version)) { max_version <- strsplit(maximum_version, "\\.")[[1]] major <- max_version[1] minor <- max_version[2] version_code <- append(version_code, c(paste("if sys.version_info.major >", major, ": raise Exception('Major version too high')"), paste("if sys.version_info.major ==", major, "and sys.version_info.minor >", minor, ": raise Exception('Minor version too high')"))) } version_code } findpython/NAMESPACE0000644000176200001440000000017714010325150013647 0ustar liggesusers# Generated by roxygen2: do not edit by hand export(can_find_python_cmd) export(find_python_cmd) export(is_python_sufficient) findpython/LICENSE0000644000176200001440000000013014716702470013443 0ustar liggesusersYEAR: 2024 COPYRIGHT HOLDER: Trevor L. Davis YEAR: 2014 COPYRIGHT HOLDER: Paul Gilbert findpython/NEWS.md0000644000176200001440000000221514716702470013542 0ustar liggesusersfindpython 1.0.9 ================ * `find_python_cmd()` and `is_python_sufficient()` now run `normalizePath(mustWork = FALSE)` on potential python binary paths. * `is_python_sufficient()` now uses `system2()` instead of `system()`. findpython 1.0.8 ================ * Looks for more python binaries (Python 3.13 through 3.20). This will help future-proof the package for later Python releases. findpython 1.0.7 ================ * Looks for more python binaries (Python 3.10 through 3.12). findpython 1.0.5 ================ * Minor internal refactoring and documentation improvements. findpython 1.0.4 ================ * No longer throws an unintended error when can't find a suitable python command AND ``reticulate`` package is not installed. findpython 1.0.3 ================ * Uses ``reticulate::py_discover_config()`` to find more python binaries if can't otherwise find a suitable python command. findpython 1.0.2 ================ * Looks for more python binaries and likely folders in Windows (i.e. Python 3.5 through Python 3.9). Thanks Jori Liesenborgs for request. findpython 1.0.1 ================ * Initial released version on CRAN. findpython/README.md0000644000176200001440000001023214716702470013721 0ustar liggesusers# findpython [![CRAN Status Badge](https://www.r-pkg.org/badges/version/findpython)](https://cran.r-project.org/package=findpython) [![R-CMD-check](https://github.com/trevorld/findpython/workflows/R-CMD-check/badge.svg)](https://github.com/trevorld/findpython/actions) [![Coverage Status](https://codecov.io/github/trevorld/findpython/branch/master/graph/badge.svg)](https://app.codecov.io/gh/trevorld/findpython) [![RStudio CRAN mirror downloads](https://cranlogs.r-pkg.org/badges/findpython)](https://cran.r-project.org/package=findpython) `findpython` is an R package that finds acceptable Python binaries for your program. Since there are often multiple python binaries installed on any given system and they aren't always added to the path this can be a non-trivial task. To install the latest version released to CRAN use: ```r install.packages("findpython") ``` To install the development version use: ```r remotes::install_github("trevorld/findpython") ``` It has no dependencies (other than R) but if you have the suggested `reticulate` package installed it will also use it to try to find an acceptable python binary. You'll also need the suggested `testthat` package to run the unit tests. ## Usage `find_python_cmd()` is the main function. It returns the path to a python binary that meets certain requirements you specify. Below are some examples. If you need to find a Python 2 binary which is at least Python 2.4: ```r library("findpython") find_python_cmd(minimum_version = '2.4', maximum_version = '2.7') ``` ```r ## [1] "/usr/bin/python" ``` If you need to find a version of Python at least Python 2.5 (but don\'t care if it is a Python 3 binary): ``` r library("findpython") find_python_cmd(minimum_version = '2.5') ``` ``` ## [1] "/usr/bin/python3.11" ``` If you don't care what version of Python you use but it needs to have access to an `argparse` module as well as either the `json` OR `simplejson` module: ``` r library("findpython") find_python_cmd(required_modules = c('argparse', 'json | simplejson')) ``` ``` ## [1] "/usr/bin/python3.11" ``` Although `find_python_cmd()` will create a basic default message if left unspecified you can use the `error_message` argument to specify what error message your program will output if it is unable to find an acceptable binary: ``` r library("findpython") error_message <- paste('Was unable to find the Python 4 binary dependency.', 'See file INSTALL for more information.') find_python_cmd(minimum_version = '4.0', error_message = error_message) ``` ``` ## Error in find_python_cmd(minimum_version = "4.0", error_message = error_message): Was unable to find the Python 4 binary dependency. See file INSTALL for more information. ``` There is also a wrapper for `find_python_cmd()` that instead of throwing an error upon failing to find an appropriate Python command will return `FALSE` and if it finds an appropriate command will return `TRUE`. If successful it attaches the appropriate binary path as an attribute `python_cmd`: ``` r library("findpython") did_find_python <- can_find_python_cmd() python_cmd <- attr(did_find_python, "python_cmd") cat(did_find_python, python_cmd, "\n") ``` ``` ## TRUE /usr/bin/python3.11 ``` The default error message will be something like: ``` r library("findpython") find_python_cmd(min='4.0') ``` ``` ## Error in find_python_cmd(min = "4.0"): Couldn't find a sufficient Python binary. If you haven't installed the Python dependency yet please do so. If you have but it isn't on the system path (as is default on Windows) please add it to path or set options('python_cmd'='/path/to/binary') or set the PYTHON, PYTHON2, or PYTHON3 environmental variables. Python must be at least version 4.0 ``` If you already have a python binary you want to check you can use `is_python_sufficient()` to test whether it has sufficient features. It has the same arguments `minimum_version`, `maximum_version`, and `required_modules` as `find_python_cmd()`: ``` r library("findpython") is_python_sufficient("python3", minimum_version = '2.6', required_modules = 'scipy') ``` ``` ## [1] TRUE ``` ## License This package is available under the [MIT](https://www.r-project.org/Licenses/MIT) license. findpython/man/0000755000176200001440000000000014716702470013217 5ustar liggesusersfindpython/man/is_python_sufficient.Rd0000644000176200001440000000265214716702470017746 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/find_python_cmd.r \name{is_python_sufficient} \alias{is_python_sufficient} \title{Tests whether the python command is sufficient} \usage{ is_python_sufficient( path, minimum_version = NULL, maximum_version = NULL, required_modules = NULL ) } \arguments{ \item{path}{The path to a given python binary. If binary is on system path just the binary name will work.} \item{minimum_version}{The minimum version of python it should be. Should be a string with major and minor number separated by a \code{.}. If left NULL won't impose such a restriction.} \item{maximum_version}{The maximum version of python it should be. Should be a string with major and minor number separated by a \code{.}. If left NULL won't impose such a restriction.} \item{required_modules}{Which modules should be required. Can use a single \code{|} to represent a single either-or requirement like \code{"json|simplejson"}. If left \code{NULL} won't impose such a restriction.} } \value{ \code{TRUE} or \code{FALSE} depending on whether the python binary met all requirements } \description{ \code{is_python_sufficient()} checks whether a given python binary has all the desired features (minimum and/or maximum version number and/or access to certain modules). } \examples{ try({ cmd <- find_python_cmd() is_python_sufficient(cmd, minimum_version = "3.3", required_modules = "sys") }) } findpython/man/find_python_cmd.Rd0000644000176200001440000000310214716702470016646 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/find_python_cmd.r \name{find_python_cmd} \alias{find_python_cmd} \title{Find a suitable python cmd or give error if not possible} \usage{ find_python_cmd( minimum_version = NULL, maximum_version = NULL, required_modules = NULL, error_message = NULL ) } \arguments{ \item{minimum_version}{The minimum version of python it should be. Should be a string with major and minor number separated by a \code{.}. If left NULL won't impose such a restriction.} \item{maximum_version}{The maximum version of python it should be. Should be a string with major and minor number separated by a \code{.}. If left NULL won't impose such a restriction.} \item{required_modules}{Which modules should be required. Can use a single \code{|} to represent a single either-or requirement like \code{"json|simplejson"}. If left \code{NULL} won't impose such a restriction.} \item{error_message}{What error message the user will see if couldn't find a sufficient python binary. If left NULL will print out a default message.} } \value{ The path to an appropriate python binary. If such a path wasn't found then it will throw an error. } \description{ \code{find_python_cmd()} finds a suitable python cmd or raises an error if not possible } \examples{ try(find_python_cmd()) try(find_python_cmd(minimum_version = "2.6", maximum_version = "2.7")) try(find_python_cmd(required_modules = c("argparse", "json | simplejson"))) } \seealso{ \code{\link[=can_find_python_cmd]{can_find_python_cmd()}} for a wrapper which doesn't throw an error } findpython/man/can_find_python_cmd.Rd0000644000176200001440000000355014716702470017476 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/find_python_cmd.r \name{can_find_python_cmd} \alias{can_find_python_cmd} \title{Determines whether or not it can find a suitable python cmd} \usage{ can_find_python_cmd( minimum_version = NULL, maximum_version = NULL, required_modules = NULL, error_message = NULL, silent = FALSE ) } \arguments{ \item{minimum_version}{The minimum version of python it should be. Should be a string with major and minor number separated by a \code{.}. If left NULL won't impose such a restriction.} \item{maximum_version}{The maximum version of python it should be. Should be a string with major and minor number separated by a \code{.}. If left NULL won't impose such a restriction.} \item{required_modules}{Which modules should be required. Can use a single \code{|} to represent a single either-or requirement like \code{"json|simplejson"}. If left \code{NULL} won't impose such a restriction.} \item{error_message}{What error message the user will see if couldn't find a sufficient python binary. If left NULL will print out a default message.} \item{silent}{Passed to \code{try}, whether any error messages from \code{\link[=find_python_cmd]{find_python_cmd()}} should be suppressed} } \value{ \code{TRUE} or \code{FALSE} depending on whether \code{\link[=find_python_cmd]{find_python_cmd()}} could find an appropriate python binary. If \code{TRUE} the path to an appropriate python binary is also set as an attribute. } \description{ \code{can_find_python_cmd()} runs \code{\link[=find_python_cmd]{find_python_cmd()}} and returns whether it could find a suitable python cmd. If it was successful its output also saves the found command as an attribute. } \examples{ did_find_cmd <- can_find_python_cmd() python_cmd <- attr(did_find_cmd, "python_cmd") } \seealso{ \code{\link[=find_python_cmd]{find_python_cmd()}} } findpython/DESCRIPTION0000644000176200001440000000165014717032436014153 0ustar liggesusersPackage: findpython Type: Package Title: Functions to Find an Acceptable Python Binary Version: 1.0.9 Authors@R: c(person("Trevor L.", "Davis", role=c("aut", "cre"), email="trevor.l.davis@gmail.com", comment = c(ORCID = "0000-0001-6341-4639")), person("Paul", "Gilbert", role="aut")) URL: https://github.com/trevorld/findpython, https://trevorldavis.com/R/findpython/ BugReports: https://github.com/trevorld/findpython/issues Description: Package designed to find an acceptable python binary. Suggests: reticulate, testthat License: MIT + file LICENSE Collate: 'find_python_cmd.r' RoxygenNote: 7.3.1 Encoding: UTF-8 NeedsCompilation: no Packaged: 2024-11-18 18:24:09 UTC; trevorld Author: Trevor L. Davis [aut, cre] (), Paul Gilbert [aut] Maintainer: Trevor L. Davis Repository: CRAN Date/Publication: 2024-11-19 06:50:06 UTC