├── .editorconfig ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── appveyor.yml ├── appveyor ├── install.ps1 └── setup_build_env.cmd ├── requirements-dev.txt ├── setup.cfg ├── setup.py ├── test_whichcraft.py ├── tox.ini └── whichcraft ├── __init__.py ├── __init__.pyi └── py.typed /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | htmlcov 29 | .pytest_cache 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Complexity 40 | output/*.html 41 | output/*/index.html 42 | 43 | # Sphinx 44 | docs/_build 45 | 46 | # Cookiecutter 47 | output/ 48 | boilerplate/ 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | 5 | python: 6 | - "3.7" 7 | - "3.6" 8 | - "2.7" 9 | - "pypy" 10 | matrix: 11 | include: 12 | - python: 3.7 # https://github.com/travis-ci/travis-ci/issues/9069#issuecomment-425720905 13 | dist: xenial 14 | sudo: true 15 | 16 | before_install: 17 | - pip install codecov 18 | 19 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 20 | install: pip install -r requirements-dev.txt 21 | 22 | # command to run tests, e.g. python setup.py test 23 | script: make test 24 | 25 | # command to run tests using coverage, e.g. python setup.py test 26 | script: py.test --cov=whichcraft --cov-report html 27 | 28 | after_success: 29 | - codecov 30 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Daniel Roy Greenfeld 9 | 10 | Contributors 11 | ------------ 12 | 13 | * Edward Betts (@EdwardBetts) 14 | * Nick Coghlan (@ncoghlan) 15 | * rooterkyberian (@rooterkyberian) 16 | * OhenebaAduhene (@OhenebaAduhene) 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/pydanny/whichcraft/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * Your operating system name and version. 21 | * Any details about your local setup that might be helpful in troubleshooting. 22 | * Detailed steps to reproduce the bug. 23 | 24 | Fix Bugs 25 | ~~~~~~~~ 26 | 27 | Look through the GitHub issues for bugs. Anything tagged with "bug" 28 | is open to whoever wants to implement it. 29 | 30 | Implement Features 31 | ~~~~~~~~~~~~~~~~~~ 32 | 33 | Look through the GitHub issues for features. Anything tagged with "feature" 34 | is open to whoever wants to implement it. 35 | 36 | Write Documentation 37 | ~~~~~~~~~~~~~~~~~~~ 38 | 39 | whichcraft could always use more documentation, whether as part of the 40 | official whichcraft docs, in docstrings, or even on the web in blog posts, 41 | articles, and such. 42 | 43 | Submit Feedback 44 | ~~~~~~~~~~~~~~~ 45 | 46 | The best way to send feedback is to file an issue at https://github.com/pydanny/whichcraft/issues. 47 | 48 | If you are proposing a feature: 49 | 50 | * Explain in detail how it would work. 51 | * Keep the scope as narrow as possible, to make it easier to implement. 52 | * Remember that this is a volunteer-driven project, and that contributions 53 | are welcome :) 54 | 55 | Get Started! 56 | ------------ 57 | 58 | Ready to contribute? Here's how to set up `whichcraft` for local development. 59 | 60 | 1. Fork the `whichcraft` repo on GitHub. 61 | 2. Clone your fork locally:: 62 | 63 | $ git clone git@github.com:your_name_here/whichcraft.git 64 | 65 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 66 | 67 | $ mkvirtualenv whichcraft 68 | $ cd whichcraft/ 69 | $ python setup.py develop 70 | 71 | 4. Create a branch for local development:: 72 | 73 | $ git checkout -b name-of-your-bugfix-or-feature 74 | 75 | Now you can make your changes locally. 76 | 77 | 5. Run `black` (python 3.6 or highter) when making any changes to enforce code style:: 78 | 79 | $ black . 80 | 81 | 5. When you're done making changes, check the tests, including testing other Python versions with tox:: 82 | 83 | $ pytest 84 | $ tox 85 | 86 | To get pytest and tox, just pip install them into your virtualenv. 87 | 88 | 6. Commit your changes and push your branch to GitHub:: 89 | 90 | $ git add . 91 | $ git commit -m "Your detailed description of your changes." 92 | $ git push origin name-of-your-bugfix-or-feature 93 | 94 | 7. Submit a pull request through the GitHub website. 95 | 96 | Pull Request Guidelines 97 | ----------------------- 98 | 99 | Before you submit a pull request, check that it meets these guidelines: 100 | 101 | 1. The pull request should include tests. 102 | 2. If the pull request adds functionality, the docs should be updated. Put 103 | your new functionality into a function with a docstring, and add the 104 | feature to the list in README.rst. 105 | 3. The pull request should work for Python 2.7, 3.3, 3.4, 3.5, 3.6, 3.7 and for PyPy. Check 106 | https://travis-ci.org/pydanny/whichcraft/pull_requests 107 | and make sure that the tests pass for all supported Python versions. 108 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | History 2 | ========= 3 | 4 | 1.0.0 (2023-09-15) 5 | --------------------- 6 | 7 | * Add type hints thanks to @thatfloflo 8 | 9 | 0.6.1 (2019-09-06) 10 | --------------------- 11 | 12 | * Fix versioning issue 13 | 14 | 0.6.0 (2019-07-12) 15 | --------------------- 16 | 17 | * Remove lingering unicode issues 18 | * Add BriteCore as a sponsor 19 | 20 | 0.5.3 (2018-10-10) 21 | --------------------- 22 | 23 | * Add BriteCore as a sponsor 24 | 25 | 0.5.2 (2018-10-09) 26 | --------------------- 27 | 28 | * Remove any mention of 3.2 and 3.3 29 | 30 | 0.5.1 (2018-10-09) 31 | --------------------- 32 | 33 | * Fix setup.py so it works with older Python 34 | 35 | 0.5.0 (2018-10-09) 36 | --------------------- 37 | 38 | * Add 3.7 support thanks to @rooterkyberian 39 | * Remove any mention of 2.6 40 | 41 | 0.4.2 (2018-04-16) 42 | --------------------- 43 | 44 | * Use black for code formatting 45 | * Move status to production/stable 46 | * Drop Python 2.6 and 3.3 support 47 | 48 | 0.4.1 (2017-04-25) 49 | --------------------- 50 | 51 | * Added tests to support Python 3.6 52 | 53 | 0.3.1 (2016-05-10) 54 | --------------------- 55 | 56 | * Now testing for `which` directly, so we can support versions of Python 3 before 3.3 (@nickcoghlan) 57 | 58 | 0.3.1 (2016-04-24) 59 | --------------------- 60 | 61 | * Correcting version in whichcraft.py 62 | 63 | 0.3.0 (2016-04-24) 64 | --------------------- 65 | 66 | * Include tests in release source tarball (@Edwardbetts) 67 | 68 | 0.2.0 (2016-04-23) 69 | --------------------- 70 | 71 | * Python 3.5 compatability 72 | 73 | 0.1.1 (2015-09-09) 74 | --------------------- 75 | 76 | * Added lyrics 77 | 78 | 0.1.0 (2015-09-09) 79 | --------------------- 80 | 81 | * First release on PyPI. 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016, Daniel Roy Greenfeld 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of whichcraft nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | include test_whichcraft.py 7 | 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | py.test --cov=whichcraft --cov-report html 3 | open htmlcov/index.html | xdg-open htmlcov/index.html 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | whichcraft 3 | =============================== 4 | 5 | .. image:: https://badge.fury.io/py/whichcraft.svg 6 | :target: http://badge.fury.io/py/whichcraft 7 | 8 | .. image:: https://travis-ci.org/pydanny/whichcraft.svg?branch=master 9 | :target: https://travis-ci.org/pydanny/whichcraft 10 | 11 | .. image:: https://codecov.io/gh/pydanny/whichcraft/branch/master/graph/badge.svg 12 | :target: http://codecov.io/github/pydanny/whichcraft?branch=master 13 | 14 | .. image:: https://ci.appveyor.com/api/projects/status/v9coijayykhkeu4d?svg=true 15 | :target: https://ci.appveyor.com/project/pydanny/whichcraft 16 | 17 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 18 | :target: https://github.com/ambv/black 19 | :alt: Code style: black 20 | 21 | :: 22 | 23 | That code in my care 24 | That sly command-line stare 25 | That strips my operating system bare 26 | It's whichcraft 27 | 28 | This package provides cross-platform cross-python ``shutil.which`` functionality. 29 | 30 | Usage 31 | ===== 32 | 33 | On Linux, Mac, Windows for Python 2.7 or any of the maintained 3s: 34 | 35 | .. code-block:: python 36 | 37 | >>> from whichcraft import which 38 | >>> which('date') 39 | '/bin/date' 40 | >>> which('calendar') 41 | '/bin/calendar' 42 | >>> which('cookiecutter') 43 | '/Users/pydanny/.envs/fun/bin/cookiecutter' 44 | >>> which('a-made-up-name') is None 45 | True 46 | 47 | 48 | Notes 49 | ===== 50 | 51 | This is a shim of the ``shutil.which`` function that's designed to work across 52 | multiple versions of Python and inside of windows. The code for Python 2.x is 53 | based on Python 3 code that I extracted from source. I originally did this for 54 | Cookiecutter_ but pulled it out in order to reduce line count for that project. 55 | 56 | Edgecase: Date function works perfectly on mac os and linux system, hence returns string. 57 | But is an in-built function in windows hence returns none as value when called in 58 | windows. 59 | 60 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 61 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - PYTHON: "C:\\Python27" 4 | PYTHON_VERSION: "2.7.11" 5 | PYTHON_ARCH: "32" 6 | TOX_ENV: "py27" 7 | 8 | - PYTHON: "C:\\Python34" 9 | PYTHON_VERSION: "3.4.3" 10 | PYTHON_ARCH: "32" 11 | TOX_ENV: "py34" 12 | 13 | - PYTHON: "C:\\Python35" 14 | PYTHON_VERSION: "3.5.0" 15 | PYTHON_ARCH: "32" 16 | TOX_ENV: "py35" 17 | 18 | - PYTHON: "C:\\Python36" 19 | PYTHON_VERSION: "3.6.0" 20 | PYTHON_ARCH: "32" 21 | TOX_ENV: "py36" 22 | 23 | - PYTHON: "C:\\Python37" 24 | PYTHON_VERSION: "3.7.0" 25 | PYTHON_ARCH: "32" 26 | TOX_ENV: "py37" 27 | 28 | init: 29 | - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" 30 | 31 | install: 32 | - "appveyor/setup_build_env.cmd" 33 | - "powershell appveyor/install.ps1" 34 | 35 | build: false # Not a C# project, build stuff at the test step instead. 36 | 37 | test_script: 38 | - "%PYTHON%/Scripts/tox -e %TOX_ENV%" 39 | 40 | after_test: 41 | - "%PYTHON%/python setup.py bdist_wheel" 42 | - ps: "ls dist" 43 | 44 | artifacts: 45 | - path: dist\* 46 | 47 | #on_success: 48 | # - TODO: upload the content of dist/*.whl to a public wheelhouse 49 | -------------------------------------------------------------------------------- /appveyor/install.ps1: -------------------------------------------------------------------------------- 1 | # Sample script to install Python and pip under Windows 2 | # Authors: Olivier Grisel and Kyle Kastner 3 | # License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ 4 | 5 | $BASE_URL = "https://www.python.org/ftp/python/" 6 | $GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" 7 | $GET_PIP_PATH = "C:\get-pip.py" 8 | 9 | 10 | function DownloadPython ($python_version, $platform_suffix) { 11 | $webclient = New-Object System.Net.WebClient 12 | $filename = "python-" + $python_version + $platform_suffix + ".msi" 13 | $url = $BASE_URL + $python_version + "/" + $filename 14 | 15 | $basedir = $pwd.Path + "\" 16 | $filepath = $basedir + $filename 17 | if (Test-Path $filename) { 18 | Write-Host "Reusing" $filepath 19 | return $filepath 20 | } 21 | 22 | # Download and retry up to 5 times in case of network transient errors. 23 | Write-Host "Downloading" $filename "from" $url 24 | $retry_attempts = 3 25 | for($i=0; $i -lt $retry_attempts; $i++){ 26 | try { 27 | $webclient.DownloadFile($url, $filepath) 28 | break 29 | } 30 | Catch [Exception]{ 31 | Start-Sleep 1 32 | } 33 | } 34 | Write-Host "File saved at" $filepath 35 | return $filepath 36 | } 37 | 38 | 39 | function InstallPython ($python_version, $architecture, $python_home) { 40 | Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home 41 | if (Test-Path $python_home) { 42 | Write-Host $python_home "already exists, skipping." 43 | return $false 44 | } 45 | if ($architecture -eq "32") { 46 | $platform_suffix = "" 47 | } else { 48 | $platform_suffix = ".amd64" 49 | } 50 | $filepath = DownloadPython $python_version $platform_suffix 51 | Write-Host "Installing" $filepath "to" $python_home 52 | $args = "/qn /i $filepath TARGETDIR=$python_home" 53 | Write-Host "msiexec.exe" $args 54 | Start-Process -FilePath "msiexec.exe" -ArgumentList $args -Wait -Passthru 55 | Write-Host "Python $python_version ($architecture) installation complete" 56 | return $true 57 | } 58 | 59 | 60 | function InstallPip ($python_home) { 61 | $pip_path = $python_home + "/Scripts/pip.exe" 62 | $python_path = $python_home + "/python.exe" 63 | if (-not(Test-Path $pip_path)) { 64 | Write-Host "Installing pip..." 65 | $webclient = New-Object System.Net.WebClient 66 | $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH) 67 | Write-Host "Executing:" $python_path $GET_PIP_PATH 68 | Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru 69 | } else { 70 | Write-Host "pip already installed." 71 | } 72 | } 73 | 74 | function InstallPackage ($python_home, $pkg) { 75 | $pip_path = $python_home + "/Scripts/pip.exe" 76 | & $pip_path install $pkg 77 | } 78 | 79 | function main () { 80 | InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON 81 | InstallPip $env:PYTHON 82 | InstallPackage $env:PYTHON tox 83 | InstallPackage $env:PYTHON wheel 84 | } 85 | 86 | main -------------------------------------------------------------------------------- /appveyor/setup_build_env.cmd: -------------------------------------------------------------------------------- 1 | :: To build extensions for 64 bit Python 3, we need to configure environment 2 | :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: 3 | :: MS Windows SDK for Windows 7 and .NET Framework 4 4 | :: 5 | :: More details at: 6 | :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows 7 | 8 | IF "%PYTHON_ARCH%"=="64" ( 9 | ECHO Configuring environment to build with MSVC on a 64bit architecture 10 | ECHO Using Windows SDK %WINDOWS_SDK_VERSION% 11 | "C:\Program Files\Microsoft SDKs\Windows\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release 12 | SET DISTUTILS_USE_SDK=1 13 | SET MSSdk=1 14 | ) ELSE ( 15 | ECHO Using default MSVC build environment for 32bit architecture 16 | ) -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | wheel>=0.23.0 3 | pytest-cov<=2.6.0 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal=1 3 | 4 | [bdist_wheel] 5 | universal=1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | 7 | try: 8 | from setuptools import setup 9 | except ImportError: 10 | from distutils.core import setup 11 | 12 | version = "0.6.1" 13 | 14 | if sys.argv[-1] == "publish": 15 | try: 16 | import wheel 17 | except ImportError: 18 | raise ImportError("Fix: pip install wheel") 19 | try: 20 | import twine 21 | except ImportError: 22 | raise ImportError("Fix: pip install twine") 23 | 24 | os.system("python setup.py sdist bdist_wheel") 25 | os.system("twine upload dist/*") 26 | print("You might want to tag a release now") 27 | sys.exit() 28 | 29 | if sys.argv[-1] == "tag": 30 | print("Tagging the version on github:") 31 | os.system("git tag -a %s -m 'version %s'" % (version, version)) 32 | os.system("git push --tags") 33 | sys.exit() 34 | 35 | readme = open("README.rst").read() 36 | history = open("HISTORY.rst").read().replace(".. :changelog:", "") 37 | 38 | 39 | def get_requirements(filename): 40 | f = open(filename).read() 41 | reqs = [ 42 | # loop through list of requirements 43 | x.strip() 44 | for x in f.splitlines() 45 | # filter out comments and empty lines 46 | if not x.strip().startswith("#") 47 | ] 48 | return reqs 49 | 50 | 51 | setup( 52 | name="whichcraft", 53 | version=version, 54 | description="""This package provides cross-platform cross-python shutil.which functionality.""", 55 | long_description=readme + "\n\n" + history, 56 | author="Daniel Roy Greenfeld", 57 | author_email="pydanny@gmail.com", 58 | url="https://github.com/pydanny/whichcraft", 59 | include_package_data=True, 60 | packages=["whichcraft"], 61 | package_data={ 62 | "whichcraft": ["__init__.pyi", "py.typed"], 63 | }, 64 | license="BSD", 65 | zip_safe=False, 66 | keywords="whichcraft", 67 | classifiers=[ 68 | "Development Status :: 5 - Production/Stable", 69 | "Intended Audience :: Developers", 70 | "License :: OSI Approved :: BSD License", 71 | "Natural Language :: English", 72 | "Programming Language :: Python :: 2", 73 | "Programming Language :: Python :: 2.7", 74 | "Programming Language :: Python :: 3", 75 | "Programming Language :: Python :: 3.6", 76 | "Programming Language :: Python :: 3.7", 77 | ], 78 | ) 79 | -------------------------------------------------------------------------------- /test_whichcraft.py: -------------------------------------------------------------------------------- 1 | ##contributors: Cupux, Hebzebba, OhenebaAduhene, amo95 2 | 3 | import os 4 | from datetime import date 5 | import pytest 6 | from whichcraft import which 7 | import sys 8 | 9 | @pytest.mark.skipif(sys.platform == "win32", reason= "Does not run on windows") 10 | def test_existing_linux(): 11 | cmd = which("date") 12 | assert cmd 13 | assert os.path.exists(cmd) 14 | assert os.access(cmd, os.F_OK | os.X_OK) 15 | assert not os.path.isdir(cmd) 16 | 17 | 18 | def test_non_existing_command(): 19 | assert which("stringthatisntashellcommand") is None 20 | 21 | @pytest.mark.skipif(sys.platform != "win32", reason= "Does run on windows") 22 | def test_existing_windows(): 23 | cmd = which("cmd") 24 | assert cmd 25 | assert os.path.exists(cmd) 26 | assert os.access(cmd, os.F_OK | os.X_OK) 27 | assert not os.path.isdir(cmd) 28 | 29 | 30 | if __name__ == "__main__": 31 | pytest.main() 32 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py36, py37 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONPATH = {toxinidir}:{toxinidir}/whichcraft 7 | commands = py.test 8 | deps = 9 | -r{toxinidir}/requirements-dev.txt 10 | -------------------------------------------------------------------------------- /whichcraft/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__ = "Daniel Roy Greenfeld" 4 | __email__ = "pydanny@gmail.com" 5 | __version__ = "0.6.1" 6 | 7 | import os 8 | import sys 9 | 10 | try: # Forced testing 11 | from shutil import which 12 | except ImportError: # Forced testing 13 | # Versions prior to Python 3.3 don't have shutil.which 14 | 15 | def which(cmd, mode=os.F_OK | os.X_OK, path=None): 16 | """Given a command, mode, and a PATH string, return the path which 17 | conforms to the given mode on the PATH, or None if there is no such 18 | file. 19 | `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result 20 | of os.environ.get("PATH"), or can be overridden with a custom search 21 | path. 22 | Note: This function was backported from the Python 3 source code. 23 | """ 24 | # Check that a given file can be accessed with the correct mode. 25 | # Additionally check that `file` is not a directory, as on Windows 26 | # directories pass the os.access check. 27 | 28 | def _access_check(fn, mode): 29 | return os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn) 30 | 31 | # If we're given a path with a directory part, look it up directly 32 | # rather than referring to PATH directories. This includes checking 33 | # relative to the current directory, e.g. ./script 34 | if os.path.dirname(cmd): 35 | if _access_check(cmd, mode): 36 | return cmd 37 | 38 | return None 39 | 40 | if path is None: 41 | path = os.environ.get("PATH", os.defpath) 42 | if not path: 43 | return None 44 | 45 | path = path.split(os.pathsep) 46 | 47 | if sys.platform == "win32": 48 | # The current directory takes precedence on Windows. 49 | if os.curdir not in path: 50 | path.insert(0, os.curdir) 51 | 52 | # PATHEXT is necessary to check on Windows. 53 | pathext = os.environ.get("PATHEXT", "").split(os.pathsep) 54 | # See if the given file matches any of the expected path 55 | # extensions. This will allow us to short circuit when given 56 | # "python.exe". If it does match, only test that one, otherwise we 57 | # have to try others. 58 | if any(cmd.lower().endswith(ext.lower()) for ext in pathext): 59 | files = [cmd] 60 | else: 61 | files = [cmd + ext for ext in pathext] 62 | else: 63 | # On other platforms you don't have things like PATHEXT to tell you 64 | # what file suffixes are executable, so just pass on cmd as-is. 65 | files = [cmd] 66 | 67 | seen = set() 68 | for dir in path: 69 | normdir = os.path.normcase(dir) 70 | if normdir not in seen: 71 | seen.add(normdir) 72 | for thefile in files: 73 | name = os.path.join(dir, thefile) 74 | if _access_check(name, mode): 75 | return name 76 | 77 | return None 78 | -------------------------------------------------------------------------------- /whichcraft/__init__.pyi: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__: str 4 | __email__: str 5 | __version__: str 6 | 7 | import os 8 | from typing import Optional 9 | 10 | try: 11 | from shutil import which 12 | except ImportError: 13 | def which( 14 | cmd: str, mode: int = ..., path: Optional[str] = ... 15 | ) -> Optional[str]: ... 16 | -------------------------------------------------------------------------------- /whichcraft/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cookiecutter/whichcraft/226393eca8b181e44d2e1ce1baf7d79d5a3a661f/whichcraft/py.typed --------------------------------------------------------------------------------