├── .githooks └── pre-commit ├── .gitignore ├── CHANGELOG.md ├── Convert-3.7.1.alfredworkflow ├── Icon.acorn ├── LICENCE ├── README.md ├── TODO ├── bin └── build ├── currencies ├── ISO 4217 List One.xlsx ├── README.md ├── currencies_crypto.py ├── currencies_crypto.tsv ├── currencies_custom.tsv ├── currencies_iso_4217.tsv ├── currencies_openexchange.py └── currencies_openexchange.tsv ├── demo.gif ├── docs └── currencies.md ├── icon.png ├── requirements.txt └── src ├── LICENCE.txt ├── active_currencies.txt.default ├── config.py ├── convert.py ├── currency.py ├── defaults.py ├── docopt.py ├── funcsigs ├── __init__.py └── version.py ├── icon.png ├── icons ├── help.png ├── money.png └── update-available.png ├── info.plist ├── info.py ├── pint ├── __init__.py ├── babel_names.py ├── compat │ ├── __init__.py │ ├── chainmap.py │ ├── lrucache.py │ ├── meta.py │ └── tokenize.py ├── constants_en.txt ├── context.py ├── converters.py ├── default_en.txt ├── default_en_0.6.txt ├── definitions.py ├── errors.py ├── formatting.py ├── matplotlib.py ├── measurement.py ├── pint_eval.py ├── quantity.py ├── registry.py ├── registry_helpers.py ├── systems.py ├── testsuite │ ├── __init__.py │ ├── helpers.py │ ├── parameterized.py │ ├── test_babel.py │ ├── test_contexts.py │ ├── test_converters.py │ ├── test_definitions.py │ ├── test_errors.py │ ├── test_formatter.py │ ├── test_infer_base_unit.py │ ├── test_issues.py │ ├── test_measurement.py │ ├── test_numpy.py │ ├── test_pint_eval.py │ ├── test_pitheorem.py │ ├── test_quantity.py │ ├── test_systems.py │ ├── test_umath.py │ ├── test_unit.py │ └── test_util.py ├── unit.py ├── util.py └── xtranslated.txt ├── pkg_resources ├── __init__.py ├── _vendor │ ├── __init__.py │ ├── appdirs.py │ ├── packaging │ │ ├── __about__.py │ │ ├── __init__.py │ │ ├── _compat.py │ │ ├── _structures.py │ │ ├── markers.py │ │ ├── requirements.py │ │ ├── specifiers.py │ │ ├── utils.py │ │ └── version.py │ ├── pyparsing.py │ └── six.py ├── extern │ └── __init__.py └── py31compat.py ├── test_convert.py ├── unit_definitions.txt ├── unit_definitions.txt.sample └── workflow ├── Notify.tgz ├── __init__.py ├── background.py ├── notify.py ├── update.py ├── util.py ├── version ├── web.py ├── workflow.py └── workflow3.py /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Reject if APP_KEY is set 4 | 5 | # _error .. | Write red "error" and args to STDERR 6 | _error() { 7 | echo $(print -P '%F{red}error%f') "$@" >&2 8 | } 9 | 10 | # _fail .. | Write red "error" and args to STDERR, then exit with status 1 11 | _fail() { 12 | _error "$@" 13 | exit 1 14 | } 15 | 16 | # _staged | Is path staged 17 | _staged() { 18 | local p="$1" 19 | git diff --name-only --cached | grep --silent "$p" 20 | return $? 21 | } 22 | 23 | exec 1>&2 # Redirect output to stderr. 24 | 25 | _staged info.plist || exit 0 26 | 27 | root="$( git rev-parse --show-toplevel )" 28 | ip="${root}/src/info.plist" 29 | 30 | # _getvar | Retrieve value for variable 31 | _getvar() { 32 | local n=$1 33 | /usr/libexec/PlistBuddy -c "Print :variables:$n" "$ip" 34 | } 35 | 36 | # Fail if info.plist can't be found 37 | test -f "$ip" || { _fail "$ip not found"; } 38 | 39 | api_key="$( _getvar APP_KEY )" 40 | 41 | test -n "$api_key" && _fail "[info.plist] APP_KEY is not empty" 42 | 43 | exit 0 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | .cache 3 | 4 | # Python snippets from gitignore on github 5 | 6 | *.py[co] 7 | 8 | # Packages 9 | *.egg 10 | *.egg-info 11 | *.dist-info 12 | dist 13 | eggs 14 | parts 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | 20 | # Installer logs 21 | pip-log.txt 22 | 23 | # Unit test / coverage reports 24 | .coverage 25 | .tox 26 | 27 | # Translations 28 | #*.mo 29 | 30 | #Mr Developer 31 | .mr.developer.cfg 32 | 33 | # Mine own 34 | *.sublime-project 35 | *.sublime-workspace 36 | .idea 37 | .tags 38 | .tags_sorted_by_file 39 | .ropeproject 40 | .project 41 | .pydevproject 42 | .settings 43 | dist 44 | *.esproj/ 45 | .svn 46 | *.swp 47 | *.swo 48 | *.bak 49 | *~ 50 | .DS_Store 51 | ._* 52 | nbproject 53 | PYSMELLTAGS 54 | PYSMELLTAGS* 55 | .sass-cache 56 | 57 | # Dropbox 58 | .dropbox.attr 59 | 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | Changelog 3 | ========= 4 | 5 | ### [3.7.0][v3.7.0] ### 6 | 7 | Released 2020-02-20 8 | 9 | - Add exchange rates from ExchangeRate-API.com (no API key required) 10 | 11 | 12 | ### [3.6.2][v3.6.2] ### 13 | 14 | Released 2019-09-06 15 | 16 | - Include `pkg_resources` (Pint dependency) 17 | 18 | 19 | ### [3.6.1][v3.6.1] ### 20 | 21 | Released 2019-05-30 22 | 23 | - Fix argument quoting 24 | 25 | 26 | ### [3.6.0][v3.6.0] ### 27 | 28 | Released 2019-05-06 29 | 30 | - Alfred 4 support. 31 | 32 | 33 | ### [3.5.3][v3.5.3] ### 34 | 35 | Released 2019-01-26 36 | 37 | - Swap `⌘C` and `↩` to match Alfred's behaviour. `↩` copies result without thousands separator and `⌘C` copies result with separator. 38 | 39 | 40 | ### [3.5.2][v3.5.2] ### 41 | 42 | Released 2019-01-26 43 | 44 | - Copy result with or without thousands separator. 45 | 46 | 47 | 48 | ### [3.5.1][v3.5.1] ### 49 | 50 | Released 2018-01-13 51 | 52 | - Add `CURRENCY_DECIMAL_PLACES` setting to provide alternate number of decimal places for currency conversions. 53 | 54 | 55 | ### [3.5][v3.5] ### 56 | 57 | Released 2018-01-12 58 | 59 | - Add `DYNAMIC_DECIMALS` setting to expand the number of decimal places until the displayed result is non-zero. 60 | 61 | 62 | ### [3.4][v3.4] ### 63 | 64 | Released 2017-12-27 65 | 66 | - Understand negative numbers in input 67 | 68 | 69 | ### [3.3.1][v3.3.1] ### 70 | 71 | Released 2017-11-21 72 | 73 | - Disable currencies with conflicting symbols 74 | 75 | 76 | ### [3.3][v3.3] ### 77 | 78 | Released 2017-11-20 79 | 80 | - Parse numbers in accordance with the decimal and thousands separators specified in the workflow configuration 81 | 82 | ### [3.2.2][v3.2.2] ### 83 | 84 | Released 2017-11-07 85 | 86 | - Show warning if user tries to convert fiat currency and `APP_KEY` isn't set 87 | - Ensure cache is cleared after user sets `APP_KEY` 88 | 89 | ### [3.2.1][v3.2.1] ### 90 | 91 | Released 2017-11-04 92 | 93 | - Clear cached data when updated from v<3.1 or when `APP_KEY` is set. 94 | 95 | 96 | ### [3.2][v3.2] ### 97 | 98 | Released 2017-11-02. 99 | 100 | - Add support for Pint's contexts #18 101 | 102 | 103 | ### [3.1][v3.1] ### 104 | 105 | Released 2017-11-02. 106 | 107 | - Replace Yahoo! Finance with [openexchangerates.org][openx] #27 108 | 109 | 110 | ### [3.0][v3.0] ### 111 | 112 | Released 2017-07-16. 113 | 114 | - Option to exclude units when copying #12 115 | - Add per-dimensionality defaults #13 116 | - Option to specify thousands separator #15 117 | - Option to specify custom decimal separator #16 118 | - Add cryptocurrencies #21 119 | - Update Alfred-Workflow 120 | - Update Pint 121 | 122 | 123 | ### [2.6][v2.6] ### 124 | 125 | Released 2017-06-15. 126 | 127 | - Fix Sierra forking issue 128 | 129 | 130 | ### [2.5][v2.5] ### 131 | 132 | Released 2015-12-11. 133 | 134 | - Fix decoding error 135 | 136 | 137 | ### [2.4][v2.4] ### 138 | 139 | Released 2015-11-28. 140 | 141 | - New money icon 142 | - Update pint, docopt and Alfred-Workflow libraries 143 | - Reorganise code and repo 144 | 145 | 146 | ### [2.3][v2.3] ### 147 | 148 | Released 2015-11-26. 149 | 150 | - Prevent currencies from clobbering existing units #7 151 | - More precise error messages 152 | - Better query parsing 153 | 154 | 155 | ### [2.2.1][v2.2.1] ### 156 | 157 | Released 2015-07-16. 158 | 159 | - Use HTTPS to fetch exchange rates #5 160 | - Improve self-updating 161 | - Use online README instead of bundled file 162 | 163 | 164 | ### [2.2][v2.2] ### 165 | 166 | Released 2015-07-15. 167 | 168 | - Add Bitcoin exchange rate #6 169 | 170 | 171 | ### 2.1 ### 172 | 173 | Never released. 174 | 175 | - Update Alfred-Workflow 176 | 177 | 178 | ### [2.0][v2.0] ### 179 | 180 | Released 2014-12-26. 181 | 182 | - Add support for 150+ currencies via Yahoo! Finance #1 #3 183 | - Add support for custom user unit definitions 184 | - Add some additional units to workflow #4 185 | - Configurable number of decimal places in results 186 | - Automatically check for updates (and offer to install them) 187 | 188 | 189 | ### [1.2][v1.2] ### 190 | 191 | Released 2014-08-19. 192 | 193 | - Properly handle units containing uppercase letters #2 194 | 195 | 196 | ### [1.1][v1.1] ### 197 | 198 | Released 2014-08-09. 199 | 200 | - First release 201 | 202 | [v1.1]: https://github.com/deanishe/alfred-convert/releases/tag/v1.1 203 | [v1.2]: https://github.com/deanishe/alfred-convert/releases/tag/v1.2 204 | [v2.0]: https://github.com/deanishe/alfred-convert/releases/tag/v2.0 205 | [v2.2.1]: https://github.com/deanishe/alfred-convert/releases/tag/v2.2.1 206 | [v2.2]: https://github.com/deanishe/alfred-convert/releases/tag/v2.2 207 | [v2.3]: https://github.com/deanishe/alfred-convert/releases/tag/v2.3 208 | [v2.4]: https://github.com/deanishe/alfred-convert/releases/tag/v2.4 209 | [v2.5]: https://github.com/deanishe/alfred-convert/releases/tag/v2.5 210 | [v2.6]: https://github.com/deanishe/alfred-convert/releases/tag/v2.6 211 | [v3.0]: https://github.com/deanishe/alfred-convert/releases/tag/v3.0 212 | [v3.1]: https://github.com/deanishe/alfred-convert/releases/tag/v3.1 213 | [v3.2]: https://github.com/deanishe/alfred-convert/releases/tag/v3.2 214 | [v3.2.1]: https://github.com/deanishe/alfred-convert/releases/tag/v3.2.1 215 | [v3.2.2]: https://github.com/deanishe/alfred-convert/releases/tag/v3.2.2 216 | [v3.3]: https://github.com/deanishe/alfred-convert/releases/tag/v3.3 217 | [v3.3.1]: https://github.com/deanishe/alfred-convert/releases/tag/v3.3.1 218 | [v3.4]: https://github.com/deanishe/alfred-convert/releases/tag/v3.4 219 | [v3.5]: https://github.com/deanishe/alfred-convert/releases/tag/v3.5 220 | [v3.5.1]: https://github.com/deanishe/alfred-convert/releases/tag/v3.5.1 221 | [v3.5.2]: https://github.com/deanishe/alfred-convert/releases/tag/v3.5.2 222 | [v3.5.3]: https://github.com/deanishe/alfred-convert/releases/tag/v3.5.3 223 | [v3.6.0]: https://github.com/deanishe/alfred-convert/releases/tag/v3.6.0 224 | [v3.6.1]: https://github.com/deanishe/alfred-convert/releases/tag/v3.6.1 225 | [v3.6.2]: https://github.com/deanishe/alfred-convert/releases/tag/v3.6.2 226 | [v3.7.0]: https://github.com/deanishe/alfred-convert/releases/tag/v3.7.0 227 | [openx]: https://openexchangerates.org/ -------------------------------------------------------------------------------- /Convert-3.7.1.alfredworkflow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-convert/97407f4ec8dbca5abbc6952b2b56cf3918624177/Convert-3.7.1.alfredworkflow -------------------------------------------------------------------------------- /Icon.acorn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-convert/97407f4ec8dbca5abbc6952b2b56cf3918624177/Icon.acorn -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | src/LICENCE.txt -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Features: 2 | ___________________ 3 | Archive: 4 | - Show list of conversions before destination unit is entered/complete. @done(17-07-16 15:24) @project(Features) 5 | - "Favourite" currencies @done(17-07-16 15:24) @project(Currency conversion) 6 | If no destination unit is supplied, and the current unit is a currency, show a list of conversions to user's favourite currencies. 7 | - Update Alfred-Workflow to fix logging rotation bug @done(15-11-28 18:30) @project(Currency conversion) 8 | - Add additional currencies @done(15-11-26 12:42) @project(Currency conversion) 9 | - Add Yahoo! lookup for currencies not in ECB list @done(15-11-26 12:42) @project(Currency conversion) 10 | - Test on McLovin without Internet connection @done(14-12-26 13:58) @project(Currency conversion) 11 | - Replace symlinked workflow directory with copy of package @done(14-12-26 13:57) @project(Deployment) 12 | - Alter workflow so currencies are loaded from cache if it exists, and updated in the background. @done(14-02-25 19:18) @project(Currency conversion) 13 | Currently, it's possible that currencies will never be loaded if there's no Internet connection. 14 | To fix this, load existing cached data and trigger background update to cached file with or without updating currently loaded data. 15 | 16 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | set -e 4 | 5 | # Paths to this script's directory and workflow root 6 | here="$( cd "$( dirname "$0" )"; pwd )" 7 | root="$( cd "$here/../"; pwd )" 8 | builddir="${root}/build" 9 | srcdir="${root}/src" 10 | 11 | noexport=(APP_KEY) 12 | 13 | verbose=false 14 | 15 | # log ... | Echo arguments to STDERR 16 | log() { 17 | echo "$@" >&2 18 | } 19 | 20 | # info .. | Write args to STDERR if VERBOSE is true 21 | info() { 22 | $verbose && log $(print -P "%F{blue}..%f") "$@" 23 | return 0 24 | } 25 | 26 | # success .. | Write green "ok" and args to STDERR if VERBOSE is true 27 | success() { 28 | $verbose && log $(print -P "%F{green}ok%f") "$@" 29 | return 0 30 | } 31 | 32 | # error .. | Write red "error" and args to STDERR 33 | error() { 34 | log $(print -P '%F{red}error%f') "$@" 35 | } 36 | 37 | # fail .. | Write red "error" and args to STDERR, then exit with status 1 38 | fail() { 39 | error "$@" 40 | exit 1 41 | } 42 | 43 | # setvar | Set value for variable 44 | setvar() { 45 | local n=$1 46 | local v=$2 47 | /usr/libexec/PlistBuddy -c "Set :variables:$n $v" "info.plist" 48 | } 49 | 50 | usage() { 51 | cat </dev/null || fail "workflow-build script not found" 84 | 85 | test -d "$builddir" && { 86 | info "cleaning builddir ..." 87 | rm -rf $vopt "$builddir/"* 88 | success "cleaned builddir" 89 | } || { 90 | mkdir $vopt "$builddir" 91 | } 92 | 93 | # mkdir $vopt "$builddir" 94 | 95 | info "copying workflow to builddir ..." 96 | rsync --archive $vopt \ 97 | -f '- *.pyc' \ 98 | -f '- .*' \ 99 | -f '- *.egg-info' \ 100 | -f '- *.dist-info' \ 101 | -f '- __pycache__' \ 102 | "$srcdir/" "$builddir/" 103 | success "copied workflow source to builddir" 104 | 105 | cd "$builddir" 106 | 107 | info "cleaning info.plist ..." 108 | for v in $noexport; do 109 | setvar "$v" "" 110 | success "unset $v" 111 | done 112 | 113 | cd - 114 | 115 | info "building workflow ..." 116 | workflow-build -f $vopt "$builddir" 117 | success "built workflow" 118 | 119 | info "clearing builddir ..." 120 | rm -rf $vopt "$builddir/"* 121 | success "done" 122 | -------------------------------------------------------------------------------- /currencies/ISO 4217 List One.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-convert/97407f4ec8dbca5abbc6952b2b56cf3918624177/currencies/ISO 4217 List One.xlsx -------------------------------------------------------------------------------- /currencies/README.md: -------------------------------------------------------------------------------- 1 | 2 | Currencies 3 | ========== 4 | 5 | Lists of currencies and scripts to generate them. 6 | 7 | | File | Description | 8 | |-------------------------------|-----------------------------------------------------------| 9 | | `currencies_crypto.tsv` | Cryptocurrencies supported by [cryptocompare.com][crypto] | 10 | | `currencies_crypto.py` | Script to generate above list | 11 | | `currencies_iso_4217.tsv` | Most ISO 4217 currency codes | 12 | | `currencies_custom.tsv` | Unofficial currencies | 13 | | `currencies_openexchange.tsv` | Exchange rates offered by [openexchangerates.org][openx] | 14 | | `currencies_openexchange.py` | Script to generate above list | 15 | | `ISO 4217 List One.xlsx` | Source list from the [ISO][iso4217] | 16 | 17 | [iso4217]: https://www.iso.org/iso-4217-currency-codes.html 18 | [openx]: https://openexchangerates.org/ 19 | [crypto]: https://www.cryptocompare.com/ 20 | -------------------------------------------------------------------------------- /currencies/currencies_crypto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2017 Dean Jackson 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2017-07-14 9 | # 10 | 11 | """ 12 | Filter list of cryptocurrencies by whether an exchange rate is available. 13 | """ 14 | 15 | from __future__ import print_function, absolute_import 16 | 17 | from collections import namedtuple 18 | import csv 19 | from itertools import izip_longest 20 | import os 21 | import requests 22 | from time import sleep 23 | 24 | reference_currency = 'USD' 25 | 26 | all_currencies_url = 'https://www.cryptocompare.com/api/data/coinlist/' 27 | base_url = 'https://min-api.cryptocompare.com/data/price?fsym={}&tsyms={}' 28 | 29 | crypto_currencies_file = os.path.join(os.path.dirname(__file__), 30 | 'currencies_crypto.tsv') 31 | 32 | 33 | Currency = namedtuple('Currency', 'symbol name') 34 | 35 | 36 | def grouper(n, iterable): 37 | """Return iterable that splits `iterable` into groups of size `n`. 38 | 39 | Args: 40 | n (int): Size of each group. 41 | iterable (iterable): The iterable to split into groups. 42 | 43 | Returns: 44 | list: Tuples of length `n` containing items 45 | from `iterable`. 46 | 47 | """ 48 | sentinel = object() 49 | args = [iter(iterable)] * n 50 | groups = [] 51 | for l in izip_longest(*args, fillvalue=sentinel): 52 | groups.append([v for v in l if v is not sentinel]) 53 | return groups 54 | 55 | 56 | def main(): 57 | """Generate list of cryptocurrencies with exchange rates.""" 58 | r = requests.get(all_currencies_url) 59 | r.raise_for_status() 60 | data = r.json() 61 | all_currencies = [] 62 | valid = [] 63 | invalid = set() 64 | for k, d in data['Data'].items(): 65 | all_currencies.append(Currency(k, d['CoinName'])) 66 | 67 | print('%d total currencies' % len(all_currencies)) 68 | 69 | # for c in sorted(all_currencies): 70 | for currencies in grouper(20, all_currencies): 71 | url = base_url.format(reference_currency, 72 | ','.join([c.symbol for c in currencies])) 73 | r = requests.get(url) 74 | r.raise_for_status() 75 | data = r.json() 76 | for c in currencies: 77 | if c.symbol in data: 78 | valid.append(c) 79 | print('[%s] OK' % c.symbol) 80 | else: 81 | invalid.add(c) 82 | print('[%s] ERROR' % c.symbol) 83 | 84 | sleep(0.3) 85 | 86 | # valid = [c for c in all_currencies if c.symbol not in invalid] 87 | with open(crypto_currencies_file, 'wb') as fp: 88 | w = csv.writer(fp, delimiter='\t') 89 | for c in sorted(valid, key=lambda t: t.symbol): 90 | r = [c.symbol.encode('utf-8'), c.name.encode('utf-8')] 91 | w.writerow(r) 92 | 93 | 94 | if __name__ == '__main__': 95 | main() 96 | -------------------------------------------------------------------------------- /currencies/currencies_custom.tsv: -------------------------------------------------------------------------------- 1 | CNH Chinese yuan (Hong Kong) 2 | CNT Chinese yuan (Taiwan) 3 | GGP Guernsey pound 4 | IMP Manx pound 5 | JEP Jersey pound 6 | KID Kiribati dollar 7 | NIS New Israeli Shekel 8 | NTD New Taiwan Dollar 9 | SLS Somaliland shilling 10 | TVD Tuvalu dollar -------------------------------------------------------------------------------- /currencies/currencies_iso_4217.tsv: -------------------------------------------------------------------------------- 1 | AED UAE Dirham 2 | AFN Afghani 3 | ALL Lek 4 | AMD Armenian Dram 5 | ANG Netherlands Antillean Guilder 6 | AOA Kwanza 7 | ARS Argentine Peso 8 | AUD Australian Dollar 9 | AWG Aruban Florin 10 | AZN Azerbaijan Manat 11 | BAM Convertible Mark 12 | BBD Barbados Dollar 13 | BDT Taka 14 | BGN Bulgarian Lev 15 | BHD Bahraini Dinar 16 | BIF Burundi Franc 17 | BMD Bermudian Dollar 18 | BND Brunei Dollar 19 | BOB Boliviano 20 | BOV Mvdol 21 | BRL Brazilian Real 22 | BSD Bahamian Dollar 23 | BTN Ngultrum 24 | BWP Pula 25 | BYN Belarusian Ruble 26 | BZD Belize Dollar 27 | CAD Canadian Dollar 28 | CDF Congolese Franc 29 | CHE WIR Euro 30 | CHF Swiss Franc 31 | CHW WIR Franc 32 | CLF Unidad de Fomento 33 | CLP Chilean Peso 34 | CNY Yuan Renminbi 35 | COP Colombian Peso 36 | COU Unidad de Valor Real 37 | CRC Costa Rican Colon 38 | CUC Peso Convertible 39 | CUP Cuban Peso 40 | CVE Cabo Verde Escudo 41 | CZK Czech Koruna 42 | DJF Djibouti Franc 43 | DKK Danish Krone 44 | DOP Dominican Peso 45 | DZD Algerian Dinar 46 | EGP Egyptian Pound 47 | ERN Nakfa 48 | ETB Ethiopian Birr 49 | EUR Euro 50 | FJD Fiji Dollar 51 | FKP Falkland Islands Pound 52 | GBP Pound Sterling 53 | GEL Lari 54 | GHS Ghana Cedi 55 | GIP Gibraltar Pound 56 | GMD Dalasi 57 | GNF Guinean Franc 58 | GTQ Quetzal 59 | GYD Guyana Dollar 60 | HKD Hong Kong Dollar 61 | HNL Lempira 62 | HRK Kuna 63 | HTG Gourde 64 | HUF Forint 65 | IDR Rupiah 66 | ILS New Israeli Sheqel 67 | INR Indian Rupee 68 | IQD Iraqi Dinar 69 | IRR Iranian Rial 70 | ISK Iceland Krona 71 | JMD Jamaican Dollar 72 | JOD Jordanian Dinar 73 | JPY Yen 74 | KES Kenyan Shilling 75 | KGS Som 76 | KHR Riel 77 | KMF Comorian Franc 78 | KPW North Korean Won 79 | KRW Won 80 | KWD Kuwaiti Dinar 81 | KYD Cayman Islands Dollar 82 | KZT Tenge 83 | LAK Lao Kip 84 | LBP Lebanese Pound 85 | LKR Sri Lanka Rupee 86 | LRD Liberian Dollar 87 | LSL Loti 88 | LYD Libyan Dinar 89 | MAD Moroccan Dirham 90 | MDL Moldovan Leu 91 | MGA Malagasy Ariary 92 | MKD Denar 93 | MMK Kyat 94 | MNT Tugrik 95 | MOP Pataca 96 | MRO Ouguiya 97 | MUR Mauritius Rupee 98 | MVR Rufiyaa 99 | MWK Malawi Kwacha 100 | MXN Mexican Peso 101 | MXV Mexican Unidad de Inversion (UDI) 102 | MYR Malaysian Ringgit 103 | MZN Mozambique Metical 104 | NAD Namibia Dollar 105 | NGN Naira 106 | NIO Cordoba Oro 107 | NOK Norwegian Krone 108 | NPR Nepalese Rupee 109 | NZD New Zealand Dollar 110 | OMR Rial Omani 111 | PAB Balboa 112 | PEN Sol 113 | PGK Kina 114 | PHP Philippine Piso 115 | PKR Pakistan Rupee 116 | PLN Zloty 117 | PYG Guarani 118 | QAR Qatari Rial 119 | RON Romanian Leu 120 | RSD Serbian Dinar 121 | RUB Russian Ruble 122 | RWF Rwanda Franc 123 | SAR Saudi Riyal 124 | SBD Solomon Islands Dollar 125 | SCR Seychelles Rupee 126 | SDG Sudanese Pound 127 | SEK Swedish Krona 128 | SGD Singapore Dollar 129 | SHP Saint Helena Pound 130 | SLL Leone 131 | SOS Somali Shilling 132 | SRD Surinam Dollar 133 | SSP South Sudanese Pound 134 | STD Dobra 135 | SVC El Salvador Colon 136 | SYP Syrian Pound 137 | SZL Lilangeni 138 | THB Baht 139 | TJS Somoni 140 | TMT Turkmenistan New Manat 141 | TND Tunisian Dinar 142 | TOP Pa’anga 143 | TRY Turkish Lira 144 | TTD Trinidad and Tobago Dollar 145 | TWD New Taiwan Dollar 146 | TZS Tanzanian Shilling 147 | UAH Hryvnia 148 | UGX Uganda Shilling 149 | USD US Dollar 150 | USN US Dollar (Next day) 151 | UYI Uruguay Peso en Unidades Indexadas (URUIURUI) 152 | UYU Peso Uruguayo 153 | UZS Uzbekistan Sum 154 | VEF Bolívar 155 | VND Dong 156 | VUV Vatu 157 | WST Tala 158 | XAF CFA Franc BEAC 159 | XAG Silver 160 | XAU Gold 161 | XBA Bond Markets Unit European Composite Unit (EURCO) 162 | XBB Bond Markets Unit European Monetary Unit (E.M.U.-6) 163 | XBC Bond Markets Unit European Unit of Account 9 (E.U.A.-9) 164 | XBD Bond Markets Unit European Unit of Account 17 (E.U.A.-17) 165 | XCD East Caribbean Dollar 166 | XDR SDR (Special Drawing Right) 167 | XOF CFA Franc BCEAO 168 | XPD Palladium 169 | XPF CFP Franc 170 | XPT Platinum 171 | XSU Sucre 172 | XUA ADB Unit of Account 173 | YER Yemeni Rial 174 | ZAR Rand 175 | ZMW Zambian Kwacha 176 | ZWL Zimbabwe Dollar -------------------------------------------------------------------------------- /currencies/currencies_openexchange.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2017 Dean Jackson 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2017-11-02 9 | # 10 | 11 | """ 12 | Take list of all/wanted currencies and filter it by whether 13 | openexchangerates.org offers exchange rates. 14 | """ 15 | 16 | from __future__ import print_function, absolute_import 17 | 18 | import csv 19 | import os 20 | import requests 21 | import sys 22 | 23 | reference_currency = 'USD' 24 | api_key = None 25 | 26 | api_url = 'https://openexchangerates.org/api/latest.json?app_id={api_key}' 27 | 28 | # Source files 29 | currency_source_files = [ 30 | os.path.join(os.path.dirname(__file__), n) for n in [ 31 | 'currencies_iso_4217.tsv', 32 | 'currencies_custom.tsv', 33 | ] 34 | ] 35 | 36 | # Destination file 37 | openx_currencies_file = os.path.join(os.path.dirname(__file__), 38 | 'currencies_openexchange.tsv') 39 | 40 | 41 | def log(s, *args): 42 | """Simple STDERR logger.""" 43 | if args: 44 | s = s % args 45 | print(s, file=sys.stderr) 46 | 47 | 48 | def load_currencies(*filepaths): 49 | """Read currencies from TSV files. 50 | 51 | Args: 52 | *filepaths: TSV files containing currencies, e.g. 53 | `XXX Currency Name` 54 | 55 | Returns: 56 | dict: `{symbol: name}` mapping of currencies. 57 | """ 58 | currencies = {} 59 | for filepath in filepaths: 60 | with open(filepath, 'rb') as fp: 61 | reader = csv.reader(fp, delimiter=b'\t') 62 | for row in reader: 63 | symbol, name = [unicode(s, 'utf-8') for s in row] 64 | currencies[symbol] = name 65 | 66 | return currencies 67 | 68 | 69 | def get_exchange_rates(symbols): 70 | """Fetch exchange rates from openexchangerates.org. 71 | 72 | Args: 73 | symbols: Set of currency symbols to return. 74 | 75 | Returns: 76 | dict: `{symbol: rate}` mapping, e.g. `{'USD': 0.9}` 77 | """ 78 | rates = {} 79 | wanted = set(symbols) 80 | url = api_url.format(api_key=api_key) 81 | r = requests.get(url) 82 | r.raise_for_status() 83 | 84 | data = r.json() 85 | for sym, rate in data['rates'].items(): 86 | if sym not in wanted: 87 | log('unwanted currency: %s', sym) 88 | continue 89 | 90 | rates[sym] = rate 91 | 92 | return rates 93 | 94 | 95 | def main(): 96 | """Generate list of currencies supported by Yahoo! Finance.""" 97 | unknown_currencies = [] 98 | all_currencies = load_currencies(*currency_source_files) 99 | 100 | to_check = all_currencies 101 | log('%d currencies to check ...', len(to_check)) 102 | 103 | rates = get_exchange_rates(to_check) 104 | for symbol in sorted(rates): 105 | rate = rates[symbol] 106 | if rate == 0: 107 | unknown_currencies.append(symbol) 108 | else: 109 | print('{0}\t{1}'.format(symbol, rate)) 110 | 111 | if len(unknown_currencies): 112 | log('\n\nUnsupported currencies:') 113 | log('-----------------------') 114 | for symbol in unknown_currencies: 115 | log('%s\t%s', symbol, all_currencies[symbol]) 116 | 117 | supported_currencies = {k: v for k, v in all_currencies.items() 118 | if k not in unknown_currencies} 119 | 120 | with open(openx_currencies_file, 'wb') as fp: 121 | w = csv.writer(fp, delimiter=b'\t') 122 | for sym in sorted(supported_currencies): 123 | name = supported_currencies[sym] 124 | r = [sym.encode('utf-8'), name.encode('utf-8')] 125 | w.writerow(r) 126 | 127 | 128 | if __name__ == '__main__': 129 | if len(sys.argv) != 2: 130 | print('Usage: currencies_openexchange.py ') 131 | sys.exit(1) 132 | api_key = sys.argv[1] 133 | main() 134 | -------------------------------------------------------------------------------- /currencies/currencies_openexchange.tsv: -------------------------------------------------------------------------------- 1 | AED UAE Dirham 2 | AFN Afghani 3 | ALL Lek 4 | AMD Armenian Dram 5 | ANG Netherlands Antillean Guilder 6 | AOA Kwanza 7 | ARS Argentine Peso 8 | AUD Australian Dollar 9 | AWG Aruban Florin 10 | AZN Azerbaijan Manat 11 | BAM Convertible Mark 12 | BBD Barbados Dollar 13 | BDT Taka 14 | BGN Bulgarian Lev 15 | BHD Bahraini Dinar 16 | BIF Burundi Franc 17 | BMD Bermudian Dollar 18 | BND Brunei Dollar 19 | BOB Boliviano 20 | BOV Mvdol 21 | BRL Brazilian Real 22 | BSD Bahamian Dollar 23 | BTC Bitcoin 24 | BTN Ngultrum 25 | BWP Pula 26 | BYN Belarusian Ruble 27 | BZD Belize Dollar 28 | CAD Canadian Dollar 29 | CDF Congolese Franc 30 | CHE WIR Euro 31 | CHF Swiss Franc 32 | CHW WIR Franc 33 | CLF Unidad de Fomento 34 | CLP Chilean Peso 35 | CNH Chinese yuan (Hong Kong) 36 | CNT Chinese yuan (Taiwan) 37 | CNY Yuan Renminbi 38 | COP Colombian Peso 39 | COU Unidad de Valor Real 40 | CRC Costa Rican Colon 41 | CUC Peso Convertible 42 | CUP Cuban Peso 43 | CVE Cabo Verde Escudo 44 | CZK Czech Koruna 45 | DJF Djibouti Franc 46 | DKK Danish Krone 47 | DOP Dominican Peso 48 | DZD Algerian Dinar 49 | EGP Egyptian Pound 50 | ERN Nakfa 51 | ETB Ethiopian Birr 52 | EUR Euro 53 | FJD Fiji Dollar 54 | FKP Falkland Islands Pound 55 | GBP Pound Sterling 56 | GEL Lari 57 | GGP Guernsey pound 58 | GHS Ghana Cedi 59 | GIP Gibraltar Pound 60 | GMD Dalasi 61 | GNF Guinean Franc 62 | GTQ Quetzal 63 | GYD Guyana Dollar 64 | HKD Hong Kong Dollar 65 | HNL Lempira 66 | HRK Kuna 67 | HTG Gourde 68 | HUF Forint 69 | IDR Rupiah 70 | ILS New Israeli Sheqel 71 | IMP Manx pound 72 | INR Indian Rupee 73 | IQD Iraqi Dinar 74 | IRR Iranian Rial 75 | ISK Iceland Krona 76 | JEP Jersey pound 77 | JMD Jamaican Dollar 78 | JOD Jordanian Dinar 79 | JPY Yen 80 | KES Kenyan Shilling 81 | KGS Som 82 | KHR Riel 83 | KID Kiribati dollar 84 | KMF Comorian Franc 85 | KPW North Korean Won 86 | KRW Won 87 | KWD Kuwaiti Dinar 88 | KYD Cayman Islands Dollar 89 | KZT Tenge 90 | LAK Lao Kip 91 | LBP Lebanese Pound 92 | LKR Sri Lanka Rupee 93 | LRD Liberian Dollar 94 | LSL Loti 95 | LYD Libyan Dinar 96 | MAD Moroccan Dirham 97 | MDL Moldovan Leu 98 | MGA Malagasy Ariary 99 | MKD Denar 100 | MMK Kyat 101 | MNT Tugrik 102 | MOP Pataca 103 | MRO Ouguiya 104 | MUR Mauritius Rupee 105 | MVR Rufiyaa 106 | MWK Malawi Kwacha 107 | MXN Mexican Peso 108 | MXV Mexican Unidad de Inversion (UDI) 109 | MYR Malaysian Ringgit 110 | MZN Mozambique Metical 111 | NAD Namibia Dollar 112 | NGN Naira 113 | NIO Cordoba Oro 114 | NIS New Israeli Shekel 115 | NOK Norwegian Krone 116 | NPR Nepalese Rupee 117 | NTD New Taiwan Dollar 118 | NZD New Zealand Dollar 119 | OMR Rial Omani 120 | PAB Balboa 121 | PEN Sol 122 | PGK Kina 123 | PHP Philippine Piso 124 | PKR Pakistan Rupee 125 | PLN Zloty 126 | PYG Guarani 127 | QAR Qatari Rial 128 | RON Romanian Leu 129 | RSD Serbian Dinar 130 | RUB Russian Ruble 131 | RWF Rwanda Franc 132 | SAR Saudi Riyal 133 | SBD Solomon Islands Dollar 134 | SCR Seychelles Rupee 135 | SDG Sudanese Pound 136 | SEK Swedish Krona 137 | SGD Singapore Dollar 138 | SHP Saint Helena Pound 139 | SLL Leone 140 | SLS Somaliland shilling 141 | SOS Somali Shilling 142 | SRD Surinam Dollar 143 | SSP South Sudanese Pound 144 | STD Dobra 145 | SVC El Salvador Colon 146 | SYP Syrian Pound 147 | SZL Lilangeni 148 | THB Baht 149 | TJS Somoni 150 | TMT Turkmenistan New Manat 151 | TND Tunisian Dinar 152 | TOP Pa’anga 153 | TRY Turkish Lira 154 | TTD Trinidad and Tobago Dollar 155 | TVD Tuvalu dollar 156 | TWD New Taiwan Dollar 157 | TZS Tanzanian Shilling 158 | UAH Hryvnia 159 | UGX Uganda Shilling 160 | USD US Dollar 161 | USN US Dollar (Next day) 162 | UYI Uruguay Peso en Unidades Indexadas (URUIURUI) 163 | UYU Peso Uruguayo 164 | UZS Uzbekistan Sum 165 | VEF Bolívar 166 | VND Dong 167 | VUV Vatu 168 | WST Tala 169 | XAF CFA Franc BEAC 170 | XAG Silver 171 | XAU Gold 172 | XBA Bond Markets Unit European Composite Unit (EURCO) 173 | XBB Bond Markets Unit European Monetary Unit (E.M.U.-6) 174 | XBC Bond Markets Unit European Unit of Account 9 (E.U.A.-9) 175 | XBD Bond Markets Unit European Unit of Account 17 (E.U.A.-17) 176 | XCD East Caribbean Dollar 177 | XDR SDR (Special Drawing Right) 178 | XOF CFA Franc BCEAO 179 | XPD Palladium 180 | XPF CFP Franc 181 | XPT Platinum 182 | XSU Sucre 183 | XUA ADB Unit of Account 184 | YER Yemeni Rial 185 | ZAR Rand 186 | ZMW Zambian Kwacha 187 | ZWL Zimbabwe Dollar 188 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-convert/97407f4ec8dbca5abbc6952b2b56cf3918624177/demo.gif -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-convert/97407f4ec8dbca5abbc6952b2b56cf3918624177/icon.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pint==0.9 2 | Alfred-Workflow==1.37.2 3 | docopt==0.6.2 4 | -------------------------------------------------------------------------------- /src/LICENCE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dean Jackson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/active_currencies.txt.default: -------------------------------------------------------------------------------- 1 | # This file contains the symbols of active currencies, one per line. 2 | # Fiat currencies are at the top, crypto currencies below. 3 | # Lines beginning with # are ignored. 4 | # 5 | # For a list of all supported currencies, use `convinfo` in Alfred and 6 | # choose "View All Supported Currencies". You can filter that list and 7 | # hit CMD+C to copy the currency symbol to paste in this file. 8 | 9 | # Fiat currencies 10 | AED 11 | AFN 12 | ALL 13 | AMD 14 | ANG 15 | AOA 16 | ARS 17 | AUD 18 | AWG 19 | AZN 20 | BAM 21 | BBD 22 | BDT 23 | BGN 24 | BHD 25 | BIF 26 | BMD 27 | BND 28 | BOB 29 | BRL 30 | BSD 31 | BTC 32 | BTN 33 | BWP 34 | BYN 35 | BYR 36 | BZD 37 | CAD 38 | CDF 39 | CHF 40 | CLF 41 | CLP 42 | CNH 43 | CNY 44 | COP 45 | CRC 46 | CUC 47 | CUP 48 | CVE 49 | CZK 50 | DJF 51 | DKK 52 | DOP 53 | DZD 54 | EEK 55 | EGP 56 | ERN 57 | ETB 58 | EUR 59 | FJD 60 | FKP 61 | GBP 62 | GEL 63 | GGP 64 | GHS 65 | GIP 66 | GMD 67 | GNF 68 | GTQ 69 | GYD 70 | HKD 71 | HNL 72 | HRK 73 | HTG 74 | HUF 75 | IDR 76 | ILS 77 | IMP 78 | INR 79 | IQD 80 | IRR 81 | ISK 82 | JEP 83 | JMD 84 | JOD 85 | JPY 86 | KES 87 | KGS 88 | KHR 89 | KMF 90 | KPW 91 | KRW 92 | KWD 93 | KYD 94 | KZT 95 | LAK 96 | LBP 97 | LKR 98 | LRD 99 | LSL 100 | LYD 101 | MAD 102 | MDL 103 | MGA 104 | MKD 105 | MMK 106 | MNT 107 | MOP 108 | MRO 109 | MRU 110 | MTL 111 | MUR 112 | MVR 113 | MWK 114 | MXN 115 | MYR 116 | MZN 117 | NAD 118 | NGN 119 | NIO 120 | NOK 121 | NPR 122 | NZD 123 | OMR 124 | PAB 125 | PEN 126 | PGK 127 | PHP 128 | PKR 129 | PLN 130 | PYG 131 | QAR 132 | RON 133 | RSD 134 | RUB 135 | RWF 136 | SAR 137 | SBD 138 | SCR 139 | SDG 140 | SEK 141 | SGD 142 | SHP 143 | SLL 144 | SOS 145 | SRD 146 | SSP 147 | STN 148 | SVC 149 | SYP 150 | SZL 151 | THB 152 | TJS 153 | TMT 154 | TND 155 | TOP 156 | TRY 157 | TTD 158 | TWD 159 | TZS 160 | UAH 161 | UGX 162 | USD 163 | UYU 164 | UZS 165 | VEF 166 | VND 167 | VUV 168 | WST 169 | XAF 170 | XAG 171 | XAU 172 | XCD 173 | XDR 174 | XOF 175 | XPD 176 | XPF 177 | XPT 178 | YER 179 | ZAR 180 | ZMW 181 | 182 | # Crypto currencies 183 | AUR 184 | BTC 185 | DASH 186 | DOGE 187 | ETC 188 | ETH 189 | LSK 190 | LTC 191 | MAID 192 | SJCX 193 | XBC 194 | XEM 195 | XMR 196 | XRP 197 | ZEC 198 | -------------------------------------------------------------------------------- /src/currency.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2014 deanishe@deanishe.net 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2014-02-24 9 | # 10 | 11 | """Script to update exchange rates in the background.""" 12 | 13 | from __future__ import print_function 14 | 15 | from itertools import izip_longest 16 | from multiprocessing.dummy import Pool 17 | import os 18 | import time 19 | 20 | from workflow import Workflow, web 21 | 22 | from config import ( 23 | bootstrap, 24 | ACTIVE_CURRENCIES_FILENAME, 25 | CURRENCY_CACHE_AGE, 26 | CURRENCY_CACHE_NAME, 27 | REFERENCE_CURRENCY, 28 | CURRENCIES, 29 | CRYPTO_CURRENCIES, 30 | CRYPTO_COMPARE_BASE_URL, 31 | OPENX_API_URL, 32 | OPENX_APP_KEY, 33 | SYMBOLS_PER_REQUEST, 34 | USER_AGENT, 35 | XRA_API_URL, 36 | ) 37 | 38 | 39 | log = None 40 | 41 | 42 | def grouper(n, iterable): 43 | """Return iterable that splits `iterable` into groups of size `n`. 44 | 45 | Args: 46 | n (int): Size of each group. 47 | iterable (iterable): The iterable to split into groups. 48 | 49 | Returns: 50 | list: Tuples of length `n` containing items 51 | from `iterable`. 52 | 53 | """ 54 | sentinel = object() 55 | args = [iter(iterable)] * n 56 | groups = [] 57 | for l in izip_longest(*args, fillvalue=sentinel): 58 | groups.append([v for v in l if v is not sentinel]) 59 | return groups 60 | 61 | 62 | def load_cryptocurrency_rates(symbols): 63 | """Return dict of exchange rates from CryptoCompare.com. 64 | 65 | Args: 66 | symbols (sequence): Abbreviations of currencies to fetch 67 | exchange rates for, e.g. 'BTC' or 'DOGE'. 68 | 69 | Returns: 70 | dict: `{symbol: rate}` mapping of exchange rates. 71 | 72 | """ 73 | url = CRYPTO_COMPARE_BASE_URL.format(REFERENCE_CURRENCY, ','.join(symbols)) 74 | 75 | log.debug('fetching %s ...', url) 76 | r = web.get(url, headers={'User-Agent': USER_AGENT}) 77 | r.raise_for_status() 78 | 79 | data = r.json() 80 | for sym, rate in data.items(): 81 | log.debug('[CryptoCompare.com] 1 %s = %s %s', 82 | REFERENCE_CURRENCY, rate, sym) 83 | 84 | return data 85 | 86 | 87 | def load_xra_rates(symbols): 88 | """Return dict of exchange rates from exchangerate-api.com. 89 | 90 | Returns: 91 | dict: `{symbol: rate}` mapping of exchange rates. 92 | 93 | """ 94 | rates = {} 95 | wanted = set(symbols) 96 | url = XRA_API_URL.format(REFERENCE_CURRENCY) 97 | r = web.get(url, headers={'User-Agent': USER_AGENT}) 98 | r.raise_for_status() 99 | log.debug('[%s] %s', r.status_code, url) 100 | data = r.json() 101 | 102 | for sym, rate in data['rates'].items(): 103 | if sym not in wanted: 104 | continue 105 | log.debug('[ExchangeRate-API.com] 1 %s = %s %s', 106 | REFERENCE_CURRENCY, rate, sym) 107 | rates[sym] = rate 108 | 109 | return rates 110 | 111 | 112 | def load_openx_rates(symbols): 113 | """Return dict of exchange rates from openexchangerates.org. 114 | 115 | Args: 116 | symbols (sequence): Abbreviations of currencies to fetch 117 | exchange rates for, e.g. 'USD' or 'GBP'. 118 | 119 | Returns: 120 | dict: `{symbol: rate}` mapping of exchange rates. 121 | 122 | """ 123 | rates = {} 124 | wanted = set(symbols) 125 | if not OPENX_APP_KEY: 126 | log.warning( 127 | 'not fetching fiat currency exchange rates: ' 128 | 'APP_KEY for openexchangerates.org not set. ' 129 | 'Please sign up for a free account here: ' 130 | 'https://openexchangerates.org/signup/free' 131 | ) 132 | return rates 133 | 134 | url = OPENX_API_URL.format(OPENX_APP_KEY) 135 | r = web.get(url, headers={'User-Agent': USER_AGENT}) 136 | r.raise_for_status() 137 | log.debug('[%s] %s', r.status_code, OPENX_API_URL.format('XXX')) 138 | data = r.json() 139 | 140 | for sym, rate in data['rates'].items(): 141 | if sym not in wanted: 142 | continue 143 | log.debug('[OpenExchangeRates.org] 1 %s = %s %s', 144 | REFERENCE_CURRENCY, rate, sym) 145 | rates[sym] = rate 146 | 147 | return rates 148 | 149 | 150 | def load_active_currencies(): 151 | """Load active currencies from user settings (or defaults). 152 | 153 | Returns: 154 | set: Symbols for active currencies. 155 | 156 | """ 157 | symbols = set() 158 | 159 | user_currencies = wf.datafile(ACTIVE_CURRENCIES_FILENAME) 160 | if not os.path.exists(user_currencies): 161 | return symbols 162 | 163 | with open(user_currencies) as fp: 164 | for line in fp: 165 | line = line.strip() 166 | if not line or line.startswith('#'): 167 | continue 168 | 169 | symbols.add(line.upper()) 170 | 171 | return symbols 172 | 173 | 174 | def fetch_exchange_rates(): 175 | """Retrieve all currency exchange rates. 176 | 177 | Batch currencies into requests of `SYMBOLS_PER_REQUEST` currencies each. 178 | 179 | Returns: 180 | list: List of `{abbr : n.nn}` dicts of exchange rates 181 | (relative to EUR). 182 | 183 | """ 184 | rates = {} 185 | futures = [] 186 | active = load_active_currencies() 187 | 188 | syms = [s for s in CURRENCIES.keys() if s in active] 189 | if not OPENX_APP_KEY: 190 | log.warning( 191 | 'fetching limited set of fiat currency exchange rates: ' 192 | 'APP_KEY for openexchangerates.org not set. ' 193 | 'Please sign up for a free account here: ' 194 | 'https://openexchangerates.org/signup/free' 195 | ) 196 | jobs = [(load_xra_rates, (syms,))] 197 | else: 198 | jobs = [(load_openx_rates, (syms,))] 199 | 200 | syms = [] 201 | for s in CRYPTO_CURRENCIES.keys(): 202 | if s in CURRENCIES: 203 | log.warning('ignoring crytopcurrency "%s", as it conflicts with ' 204 | 'a fiat currency', s) 205 | continue 206 | if s in active: 207 | syms.append(s) 208 | # syms = [s for s in CRYPTO_CURRENCIES.keys() if s in active] 209 | for symbols in grouper(SYMBOLS_PER_REQUEST, syms): 210 | jobs.append((load_cryptocurrency_rates, (symbols,))) 211 | 212 | # fetch data in a thread pool 213 | pool = Pool(2) 214 | for job in jobs: 215 | futures.append(pool.apply_async(*job)) 216 | 217 | pool.close() 218 | pool.join() 219 | 220 | for f in futures: 221 | rates.update(f.get()) 222 | 223 | return rates 224 | 225 | 226 | def main(wf): 227 | """Update exchange rates. 228 | 229 | Args: 230 | wf (workflow.Workflow): Workflow object. 231 | 232 | """ 233 | start_time = time.time() 234 | bootstrap(wf) 235 | 236 | site = 'OpenExchangeRates.org' if OPENX_APP_KEY else 'ExchangeRate-API.com' 237 | 238 | log.info('fetching exchange rates from %s and CryptoCompare.com ...', 239 | site) 240 | 241 | rates = wf.cached_data(CURRENCY_CACHE_NAME, 242 | fetch_exchange_rates, 243 | CURRENCY_CACHE_AGE) 244 | 245 | elapsed = time.time() - start_time 246 | log.info('%d exchange rates updated in %0.2f seconds.', 247 | len(rates), elapsed) 248 | 249 | for currency, rate in sorted(rates.items()): 250 | log.debug('1 %s = %s %s', REFERENCE_CURRENCY, rate, currency) 251 | 252 | 253 | if __name__ == '__main__': 254 | wf = Workflow() 255 | log = wf.logger 256 | wf.run(main) 257 | -------------------------------------------------------------------------------- /src/defaults.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2017 Dean Jackson 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2017-07-16 9 | # 10 | 11 | """defaults.py (save|delete) 12 | 13 | Save/delete default units for given dimensionality. 14 | 15 | Usage: 16 | defaults.py save 17 | defaults.py delete 18 | defaults.py --help 19 | 20 | Options: 21 | -h, --help Show this message 22 | 23 | """ 24 | 25 | from __future__ import print_function, absolute_import 26 | 27 | from collections import defaultdict 28 | 29 | from docopt import docopt 30 | from workflow import Workflow3 31 | 32 | 33 | log = None 34 | 35 | 36 | class Defaults(object): 37 | """Manage default units for dimensionalities. 38 | 39 | Saves default units in workflow's settings file. 40 | 41 | """ 42 | 43 | def __init__(self, wf): 44 | """Create new `Defaults` for workflow. 45 | 46 | Args: 47 | wf (Workflow3): Active Workflow3 object. 48 | """ 49 | self._wf = wf 50 | self._defs = self._load() 51 | 52 | def defaults(self, dimensionality): 53 | """Default units for dimensionality. 54 | 55 | Args: 56 | dimensionality (str): Dimensionality to return units for 57 | 58 | Returns: 59 | list: Sequence of default units 60 | 61 | """ 62 | return self._defs[dimensionality][:] 63 | 64 | def add(self, dimensionality, unit): 65 | """Save ``unit`` as default for ``dimensionality``. 66 | 67 | Args: 68 | dimensionality (str): Dimensionality 69 | unit (str): Unit 70 | 71 | """ 72 | if not self.is_default(dimensionality, unit): 73 | self._defs[dimensionality].append(unit) 74 | self._save() 75 | 76 | def remove(self, dimensionality, unit): 77 | """Remove ``unit`` as default for ``dimensionality``. 78 | 79 | Args: 80 | dimensionality (str): Dimensionality 81 | unit (str): Unit 82 | 83 | """ 84 | if self.is_default(dimensionality, unit): 85 | self._defs[dimensionality].remove(unit) 86 | self._save() 87 | 88 | def is_default(self, dimensionality, unit): 89 | """Check whether ``unit`` is a default for ``dimensionality``. 90 | 91 | Args: 92 | dimensionality (str): Dimensionality 93 | unit (str): Unit 94 | 95 | Returns: 96 | bool: ``True`` if ``unit`` is a default. 97 | 98 | """ 99 | return unit in self._defs[dimensionality] 100 | 101 | def _load(self): 102 | defs = defaultdict(list) 103 | defs.update(self._wf.settings.get('default_units', {})) 104 | return defs 105 | 106 | def _save(self): 107 | self._wf.settings['default_units'] = dict(self._defs) 108 | 109 | 110 | def main(wf): 111 | """Run script.""" 112 | args = docopt(__doc__, wf.args) 113 | log.debug('args=%r', args) 114 | 115 | defs = Defaults(wf) 116 | log.debug('defaults=%r', defs._defs) 117 | 118 | dimensionality = args[''] 119 | unit = args[''] 120 | 121 | if args['save']: 122 | defs.add(dimensionality, unit) 123 | print(u'Saved {} as default unit for {}'.format(unit, dimensionality)) 124 | return 125 | 126 | if args['delete']: 127 | defs.remove(dimensionality, unit) 128 | print(u'Removed {} as default unit for {}'.format(unit, dimensionality)) 129 | return 130 | 131 | 132 | if __name__ == '__main__': 133 | wf = Workflow3() 134 | log = wf.logger 135 | wf.run(main) 136 | -------------------------------------------------------------------------------- /src/funcsigs/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.2" 2 | -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-convert/97407f4ec8dbca5abbc6952b2b56cf3918624177/src/icon.png -------------------------------------------------------------------------------- /src/icons/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-convert/97407f4ec8dbca5abbc6952b2b56cf3918624177/src/icons/help.png -------------------------------------------------------------------------------- /src/icons/money.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-convert/97407f4ec8dbca5abbc6952b2b56cf3918624177/src/icons/money.png -------------------------------------------------------------------------------- /src/icons/update-available.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-convert/97407f4ec8dbca5abbc6952b2b56cf3918624177/src/icons/update-available.png -------------------------------------------------------------------------------- /src/info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2014 deanishe@deanishe.net 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2014-12-26 9 | # 10 | 11 | """info.py [options] [] 12 | 13 | View/manage workflow settings. 14 | 15 | Usage: 16 | info.py [] 17 | info.py (-h|--help) 18 | info.py --openhelp 19 | info.py --openactive 20 | info.py --openunits 21 | info.py --openapi 22 | info.py --currencies [] 23 | 24 | Options: 25 | -h, --help Show this message 26 | --openhelp Open help file in default browser 27 | --openactive Open active currency file in default editor 28 | --openunits Open custom units file in default editor 29 | --openapi Open the openexchangerates.org signup page 30 | --currencies View/search supported currencies 31 | 32 | """ 33 | 34 | from __future__ import absolute_import 35 | 36 | from datetime import timedelta 37 | import subprocess 38 | import sys 39 | 40 | from docopt import docopt 41 | 42 | from workflow import ( 43 | ICON_INFO, 44 | ICON_WARNING, 45 | ICON_WEB, 46 | MATCH_ALL, 47 | MATCH_ALLCHARS, 48 | Workflow3, 49 | ) 50 | from workflow.util import run_trigger 51 | 52 | from config import ( 53 | bootstrap, 54 | ACTIVE_CURRENCIES_FILENAME, 55 | CURRENCIES, 56 | CRYPTO_CURRENCIES, 57 | CURRENCY_CACHE_NAME, 58 | CUSTOM_DEFINITIONS_FILENAME, 59 | ICON_CURRENCY, 60 | ICON_HELP, 61 | README_URL, 62 | ) 63 | 64 | # Signup page for free API key 65 | SIGNUP_URL = 'https://openexchangerates.org/signup/free' 66 | 67 | log = None 68 | 69 | DELIMITER = u'\u203a' # SINGLE RIGHT-POINTING ANGLE QUOTATION MARK 70 | 71 | 72 | def human_timedelta(td): 73 | """Return relative time (past) in human-readable format. 74 | 75 | Example: "10 minutes ago" 76 | 77 | Args: 78 | td (datetime.timedelta): Time delta to convert. 79 | 80 | Returns: 81 | unicode: Human-readable time delta. 82 | 83 | """ 84 | output = [] 85 | d = {'day': td.days} 86 | d['hour'], rem = divmod(td.seconds, 3600) 87 | d['minute'], d['second'] = divmod(rem, 60) 88 | 89 | for unit in ('day', 'hour', 'minute', 'second'): 90 | i = d[unit] 91 | 92 | if i == 1: 93 | output.append('1 %s' % unit) 94 | 95 | elif i > 1: 96 | output.append('%d %ss' % (i, unit)) 97 | 98 | # we want to ignore only leading zero values 99 | # otherwise we'll end up with times like 100 | # "3 days and 10 seconds" 101 | elif len(output): 102 | output.append(None) 103 | 104 | if len(output) > 2: 105 | output = output[:2] 106 | 107 | # strip out "Nones" to leave only relevant units 108 | output = [s for s in output if s is not None] 109 | output.append('ago') 110 | return ' '.join(output) 111 | 112 | 113 | def handle_delimited_query(query): 114 | """Process sub-commands. 115 | 116 | Args: 117 | query (str): User query 118 | 119 | """ 120 | # Currencies or decimal places 121 | if query.endswith(DELIMITER): # User deleted trailing space 122 | run_trigger('config') 123 | # subprocess.call(['osascript', '-e', ALFRED_AS]) 124 | # return 125 | 126 | mode, query = [s.strip() for s in query.split(DELIMITER)] 127 | 128 | if mode == 'currencies': 129 | 130 | currencies = sorted([(name, symbol) for (symbol, name) 131 | in CURRENCIES.items()] + 132 | [(name, symbol) for (symbol, name) 133 | in CRYPTO_CURRENCIES.items()]) 134 | 135 | if query: 136 | currencies = wf.filter(query, currencies, 137 | key=lambda t: ' '.join(t), 138 | match_on=MATCH_ALL ^ MATCH_ALLCHARS, 139 | min_score=30) 140 | 141 | else: # Show last update time 142 | age = wf.cached_data_age(CURRENCY_CACHE_NAME) 143 | if age > 0: # Exchange rates in cache 144 | td = timedelta(seconds=age) 145 | wf.add_item('Exchange rates updated {}'.format( 146 | human_timedelta(td)), 147 | icon=ICON_INFO) 148 | 149 | if not currencies: 150 | wf.add_item('No matching currencies', 151 | 'Try a different query', 152 | icon=ICON_WARNING) 153 | 154 | for name, symbol in currencies: 155 | wf.add_item(u'{} // {}'.format(name, symbol), 156 | u'Use `{}` in conversions'.format(symbol), 157 | copytext=symbol, 158 | valid=False, 159 | icon=ICON_CURRENCY) 160 | 161 | wf.send_feedback() 162 | 163 | 164 | def main(wf): 165 | """Run Script Filter. 166 | 167 | Args: 168 | wf (workflow.Workflow): Workflow object. 169 | 170 | """ 171 | args = docopt(__doc__, wf.args) 172 | 173 | log.debug('args : {!r}'.format(args)) 174 | 175 | query = args.get('') 176 | 177 | bootstrap(wf) 178 | 179 | # Alternative actions ---------------------------------------------- 180 | 181 | if args.get('--openapi'): 182 | subprocess.call(['open', SIGNUP_URL]) 183 | return 184 | 185 | if args.get('--openhelp'): 186 | subprocess.call(['open', README_URL]) 187 | return 188 | 189 | if args.get('--openunits'): 190 | path = wf.datafile(CUSTOM_DEFINITIONS_FILENAME) 191 | subprocess.call(['open', path]) 192 | return 193 | 194 | if args.get('--openactive'): 195 | path = wf.datafile(ACTIVE_CURRENCIES_FILENAME) 196 | subprocess.call(['open', path]) 197 | return 198 | 199 | # Parse query ------------------------------------------------------ 200 | 201 | if DELIMITER in query: 202 | return handle_delimited_query(query) 203 | 204 | # Filter options --------------------------------------------------- 205 | 206 | query = query.strip() 207 | 208 | options = [ 209 | dict(title='View Help File', 210 | subtitle='Open help file in your browser', 211 | valid=True, 212 | arg='--openhelp', 213 | icon=ICON_HELP), 214 | 215 | dict(title='View All Supported Currencies', 216 | subtitle='View and search list of supported currencies', 217 | autocomplete=u'currencies {} '.format(DELIMITER), 218 | icon=ICON_CURRENCY), 219 | 220 | dict(title='Edit Active Currencies', 221 | subtitle='Edit the list of active currencies', 222 | valid=True, 223 | arg='--openactive', 224 | icon='icon.png'), 225 | 226 | dict(title='Edit Custom Units', 227 | subtitle='Add and edit your own custom units', 228 | valid=True, 229 | arg='--openunits', 230 | icon='icon.png'), 231 | 232 | dict(title='Get API key', 233 | subtitle='Sign up for free openexchangerates.org account', 234 | valid=True, 235 | arg='--openapi', 236 | icon=ICON_WEB), 237 | ] 238 | 239 | if query: 240 | options = wf.filter(query, options, key=lambda d: d['title'], 241 | min_score=30) 242 | 243 | if not options: 244 | wf.add_item('No matching options', 'Try a different query?', 245 | icon=ICON_WARNING) 246 | 247 | for d in options: 248 | wf.add_item(**d) 249 | 250 | wf.send_feedback() 251 | return 252 | 253 | 254 | if __name__ == '__main__': 255 | wf = Workflow3() 256 | log = wf.logger 257 | sys.exit(wf.run(main)) 258 | -------------------------------------------------------------------------------- /src/pint/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint 4 | ~~~~ 5 | 6 | Pint is Python module/package to define, operate and manipulate 7 | **physical quantities**: the product of a numerical value and a 8 | unit of measurement. It allows arithmetic operations between them 9 | and conversions from and to different units. 10 | 11 | :copyright: 2016 by Pint Authors, see AUTHORS for more details. 12 | :license: BSD, see LICENSE for more details. 13 | """ 14 | from __future__ import with_statement 15 | 16 | 17 | import pkg_resources 18 | from .formatting import formatter 19 | from .registry import (UnitRegistry, LazyRegistry) 20 | from .errors import (DimensionalityError, OffsetUnitCalculusError, 21 | UndefinedUnitError, UnitStrippedWarning) 22 | from .util import pi_theorem, logger 23 | 24 | from .context import Context 25 | 26 | import sys 27 | try: 28 | from pintpandas import PintType, PintArray 29 | _HAS_PINTPANDAS = True 30 | except Exception: 31 | _HAS_PINTPANDAS = False 32 | _, _pintpandas_error, _ = sys.exc_info() 33 | 34 | try: # pragma: no cover 35 | __version__ = pkg_resources.get_distribution('pint').version 36 | except: # pragma: no cover 37 | # we seem to have a local copy not installed without setuptools 38 | # so the reported version will be unknown 39 | __version__ = "unknown" 40 | 41 | 42 | #: A Registry with the default units and constants. 43 | _DEFAULT_REGISTRY = LazyRegistry() 44 | 45 | #: Registry used for unpickling operations. 46 | _APP_REGISTRY = _DEFAULT_REGISTRY 47 | 48 | 49 | def _build_quantity(value, units): 50 | """Build Quantity using the Application registry. 51 | Used only for unpickling operations. 52 | """ 53 | from .unit import UnitsContainer 54 | 55 | global _APP_REGISTRY 56 | 57 | # Prefixed units are defined within the registry 58 | # on parsing (which does not happen here). 59 | # We need to make sure that this happens before using. 60 | if isinstance(units, UnitsContainer): 61 | for name in units.keys(): 62 | _APP_REGISTRY.parse_units(name) 63 | 64 | return _APP_REGISTRY.Quantity(value, units) 65 | 66 | 67 | def _build_unit(units): 68 | """Build Unit using the Application registry. 69 | Used only for unpickling operations. 70 | """ 71 | from .unit import UnitsContainer 72 | 73 | global _APP_REGISTRY 74 | 75 | # Prefixed units are defined within the registry 76 | # on parsing (which does not happen here). 77 | # We need to make sure that this happens before using. 78 | if isinstance(units, UnitsContainer): 79 | for name in units.keys(): 80 | _APP_REGISTRY.parse_units(name) 81 | 82 | return _APP_REGISTRY.Unit(units) 83 | 84 | 85 | def set_application_registry(registry): 86 | """Set the application registry which is used for unpickling operations. 87 | 88 | :param registry: a UnitRegistry instance. 89 | """ 90 | assert isinstance(registry, UnitRegistry) 91 | global _APP_REGISTRY 92 | logger.debug('Changing app registry from %r to %r.', _APP_REGISTRY, registry) 93 | _APP_REGISTRY = registry 94 | 95 | 96 | def test(): 97 | """Run all tests. 98 | 99 | :return: a :class:`unittest.TestResult` object 100 | """ 101 | from .testsuite import run 102 | return run() 103 | -------------------------------------------------------------------------------- /src/pint/babel_names.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.babel 4 | ~~~~~~~~~~ 5 | 6 | :copyright: 2016 by Pint Authors, see AUTHORS for more details. 7 | :license: BSD, see LICENSE for more details. 8 | """ 9 | 10 | from pint.compat import HAS_PROPER_BABEL 11 | 12 | _babel_units = dict( 13 | standard_gravity='acceleration-g-force', 14 | millibar='pressure-millibar', 15 | metric_ton='mass-metric-ton', 16 | megawatt='power-megawatt', 17 | degF='temperature-fahrenheit', 18 | dietary_calorie='energy-foodcalorie', 19 | millisecond='duration-millisecond', 20 | mph='speed-mile-per-hour', 21 | acre_foot='volume-acre-foot', 22 | mebibit='digital-megabit', 23 | gibibit='digital-gigabit', 24 | tebibit='digital-terabit', 25 | mebibyte='digital-megabyte', 26 | kibibyte='digital-kilobyte', 27 | mm_Hg='pressure-millimeter-of-mercury', 28 | month='duration-month', 29 | kilocalorie='energy-kilocalorie', 30 | cubic_mile='volume-cubic-mile', 31 | arcsecond='angle-arc-second', 32 | byte='digital-byte', 33 | metric_cup='volume-cup-metric', 34 | kilojoule='energy-kilojoule', 35 | meter_per_second_squared='acceleration-meter-per-second-squared', 36 | pint='volume-pint', 37 | square_centimeter='area-square-centimeter', 38 | in_Hg='pressure-inch-hg', 39 | milliampere='electric-milliampere', 40 | arcminute='angle-arc-minute', 41 | MPG='consumption-mile-per-gallon', 42 | hertz='frequency-hertz', 43 | day='duration-day', 44 | mps='speed-meter-per-second', 45 | kilometer='length-kilometer', 46 | square_yard='area-square-yard', 47 | kelvin='temperature-kelvin', 48 | kilogram='mass-kilogram', 49 | kilohertz='frequency-kilohertz', 50 | megahertz='frequency-megahertz', 51 | meter='length-meter', 52 | cubic_inch='volume-cubic-inch', 53 | kilowatt_hour='energy-kilowatt-hour', 54 | second='duration-second', 55 | yard='length-yard', 56 | light_year='length-light-year', 57 | millimeter='length-millimeter', 58 | metric_horsepower='power-horsepower', 59 | gibibyte='digital-gigabyte', 60 | ## 'temperature-generic', 61 | liter='volume-liter', 62 | turn='angle-revolution', 63 | microsecond='duration-microsecond', 64 | pound='mass-pound', 65 | ounce='mass-ounce', 66 | calorie='energy-calorie', 67 | centimeter='length-centimeter', 68 | inch='length-inch', 69 | centiliter='volume-centiliter', 70 | troy_ounce='mass-ounce-troy', 71 | gream='mass-gram', 72 | kilowatt='power-kilowatt', 73 | knot='speed-knot', 74 | lux='light-lux', 75 | hectoliter='volume-hectoliter', 76 | microgram='mass-microgram', 77 | degC='temperature-celsius', 78 | tablespoon='volume-tablespoon', 79 | cubic_yard='volume-cubic-yard', 80 | square_foot='area-square-foot', 81 | tebibyte='digital-terabyte', 82 | square_inch='area-square-inch', 83 | carat='mass-carat', 84 | hectopascal='pressure-hectopascal', 85 | gigawatt='power-gigawatt', 86 | watt='power-watt', 87 | micrometer='length-micrometer', 88 | volt='electric-volt', 89 | bit='digital-bit', 90 | gigahertz='frequency-gigahertz', 91 | teaspoon='volume-teaspoon', 92 | ohm='electric-ohm', 93 | joule='energy-joule', 94 | cup='volume-cup', 95 | square_mile='area-square-mile', 96 | nautical_mile='length-nautical-mile', 97 | square_meter='area-square-meter', 98 | mile='length-mile', 99 | acre='area-acre', 100 | nanometer='length-nanometer', 101 | hour='duration-hour', 102 | astronomical_unit='length-astronomical-unit', 103 | liter_per_100kilometers ='consumption-liter-per-100kilometers', 104 | megaliter='volume-megaliter', 105 | ton='mass-ton', 106 | hectare='area-hectare', 107 | square_kilometer='area-square-kilometer', 108 | kibibit='digital-kilobit', 109 | mile_scandinavian='length-mile-scandinavian', 110 | liter_per_kilometer='consumption-liter-per-kilometer', 111 | century='duration-century', 112 | cubic_foot='volume-cubic-foot', 113 | deciliter='volume-deciliter', 114 | ##pint='volume-pint-metric', 115 | cubic_meter='volume-cubic-meter', 116 | cubic_kilometer='volume-cubic-kilometer', 117 | quart='volume-quart', 118 | cc='volume-cubic-centimeter', 119 | pound_force_per_square_inch='pressure-pound-per-square-inch', 120 | milligram='mass-milligram', 121 | kph='speed-kilometer-per-hour', 122 | minute='duration-minute', 123 | parsec='length-parsec', 124 | picometer='length-picometer', 125 | degree='angle-degree', 126 | milliwatt='power-milliwatt', 127 | week='duration-week', 128 | ampere='electric-ampere', 129 | milliliter='volume-milliliter', 130 | decimeter='length-decimeter', 131 | fluid_ounce='volume-fluid-ounce', 132 | nanosecond='duration-nanosecond', 133 | foot='length-foot', 134 | karat='proportion-karat', 135 | year='duration-year', 136 | gallon='volume-gallon', 137 | radian='angle-radian', 138 | ) 139 | 140 | if not HAS_PROPER_BABEL: 141 | _babel_units = dict() 142 | 143 | _babel_systems = dict( 144 | mks='metric', 145 | imperial='uksystem', 146 | US='ussystem', 147 | ) 148 | 149 | _babel_lengths = ['narrow', 'short', 'long'] 150 | 151 | -------------------------------------------------------------------------------- /src/pint/compat/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.compat 4 | ~~~~~~~~~~~ 5 | 6 | Compatibility layer. 7 | 8 | :copyright: 2013 by Pint Authors, see AUTHORS for more details. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | from __future__ import division, unicode_literals, print_function, absolute_import 13 | 14 | import sys 15 | 16 | from io import BytesIO 17 | from numbers import Number 18 | from decimal import Decimal 19 | 20 | from . import tokenize 21 | 22 | ENCODING_TOKEN = tokenize.ENCODING 23 | 24 | PYTHON3 = sys.version >= '3' 25 | 26 | def tokenizer(input_string): 27 | for tokinfo in tokenize.tokenize(BytesIO(input_string.encode('utf-8')).readline): 28 | if tokinfo.type == ENCODING_TOKEN: 29 | continue 30 | yield tokinfo 31 | 32 | 33 | if PYTHON3: 34 | string_types = str 35 | 36 | def u(x): 37 | return x 38 | 39 | maketrans = str.maketrans 40 | 41 | long_type = int 42 | else: 43 | string_types = basestring 44 | 45 | import codecs 46 | 47 | def u(x): 48 | return codecs.unicode_escape_decode(x)[0] 49 | 50 | maketrans = lambda f, t: dict((ord(a), b) for a, b in zip(f, t)) 51 | 52 | long_type = long 53 | 54 | try: 55 | from collections import Chainmap 56 | except ImportError: 57 | from .chainmap import ChainMap 58 | 59 | try: 60 | from functools import lru_cache 61 | except ImportError: 62 | from .lrucache import lru_cache 63 | 64 | try: 65 | from itertools import zip_longest 66 | except ImportError: 67 | from itertools import izip_longest as zip_longest 68 | 69 | try: 70 | import numpy as np 71 | from numpy import ndarray 72 | 73 | HAS_NUMPY = True 74 | NUMPY_VER = np.__version__ 75 | NUMERIC_TYPES = (Number, Decimal, ndarray, np.number) 76 | 77 | def _to_magnitude(value, force_ndarray=False): 78 | if isinstance(value, (dict, bool)) or value is None: 79 | raise TypeError('Invalid magnitude for Quantity: {0!r}'.format(value)) 80 | elif isinstance(value, string_types) and value == '': 81 | raise ValueError('Quantity magnitude cannot be an empty string.') 82 | elif isinstance(value, (list, tuple)): 83 | return np.asarray(value) 84 | if force_ndarray: 85 | return np.asarray(value) 86 | return value 87 | 88 | except ImportError: 89 | 90 | np = None 91 | 92 | class ndarray(object): 93 | pass 94 | 95 | HAS_NUMPY = False 96 | NUMPY_VER = '0' 97 | NUMERIC_TYPES = (Number, Decimal) 98 | 99 | def _to_magnitude(value, force_ndarray=False): 100 | if isinstance(value, (dict, bool)) or value is None: 101 | raise TypeError('Invalid magnitude for Quantity: {0!r}'.format(value)) 102 | elif isinstance(value, string_types) and value == '': 103 | raise ValueError('Quantity magnitude cannot be an empty string.') 104 | elif isinstance(value, (list, tuple)): 105 | raise TypeError('lists and tuples are valid magnitudes for ' 106 | 'Quantity only when NumPy is present.') 107 | return value 108 | 109 | try: 110 | from uncertainties import ufloat 111 | HAS_UNCERTAINTIES = True 112 | except ImportError: 113 | ufloat = None 114 | HAS_UNCERTAINTIES = False 115 | 116 | try: 117 | from babel import Locale as Loc 118 | from babel import units as babel_units 119 | HAS_BABEL = True 120 | HAS_PROPER_BABEL = hasattr(babel_units, 'format_unit') 121 | except ImportError: 122 | HAS_PROPER_BABEL = HAS_BABEL = False 123 | 124 | if not HAS_PROPER_BABEL: 125 | Loc = babel_units = None 126 | 127 | try: 128 | import pandas as pd 129 | HAS_PANDAS = True 130 | # pin Pandas version for now 131 | HAS_PROPER_PANDAS = pd.__version__.startswith("0.24.0.dev0+625.gbdb7a16") 132 | except ImportError: 133 | HAS_PROPER_PANDAS = HAS_PANDAS = False 134 | 135 | try: 136 | import pytest 137 | HAS_PYTEST = True 138 | except ImportError: 139 | HAS_PYTEST = False 140 | -------------------------------------------------------------------------------- /src/pint/compat/chainmap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.compat.chainmap 4 | ~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Taken from the Python 3.3 source code. 7 | 8 | :copyright: 2013, PSF 9 | :license: PSF License 10 | """ 11 | 12 | from __future__ import division, unicode_literals, print_function, absolute_import 13 | 14 | import sys 15 | 16 | if sys.version_info < (3, 3): 17 | from collections import MutableMapping 18 | else: 19 | from collections.abc import MutableMapping 20 | 21 | if sys.version_info < (3, 0): 22 | from thread import get_ident 23 | else: 24 | from threading import get_ident 25 | 26 | 27 | def _recursive_repr(fillvalue='...'): 28 | 'Decorator to make a repr function return fillvalue for a recursive call' 29 | 30 | def decorating_function(user_function): 31 | repr_running = set() 32 | 33 | def wrapper(self): 34 | key = id(self), get_ident() 35 | if key in repr_running: 36 | return fillvalue 37 | repr_running.add(key) 38 | try: 39 | result = user_function(self) 40 | finally: 41 | repr_running.discard(key) 42 | return result 43 | 44 | # Can't use functools.wraps() here because of bootstrap issues 45 | wrapper.__module__ = getattr(user_function, '__module__') 46 | wrapper.__doc__ = getattr(user_function, '__doc__') 47 | wrapper.__name__ = getattr(user_function, '__name__') 48 | wrapper.__annotations__ = getattr(user_function, '__annotations__', {}) 49 | return wrapper 50 | 51 | return decorating_function 52 | 53 | 54 | class ChainMap(MutableMapping): 55 | ''' A ChainMap groups multiple dicts (or other mappings) together 56 | to create a single, updateable view. 57 | 58 | The underlying mappings are stored in a list. That list is public and can 59 | accessed or updated using the *maps* attribute. There is no other state. 60 | 61 | Lookups search the underlying mappings successively until a key is found. 62 | In contrast, writes, updates, and deletions only operate on the first 63 | mapping. 64 | 65 | ''' 66 | 67 | def __init__(self, *maps): 68 | '''Initialize a ChainMap by setting *maps* to the given mappings. 69 | If no mappings are provided, a single empty dictionary is used. 70 | 71 | ''' 72 | self.maps = list(maps) or [{}] # always at least one map 73 | 74 | def __missing__(self, key): 75 | raise KeyError(key) 76 | 77 | def __getitem__(self, key): 78 | for mapping in self.maps: 79 | try: 80 | return mapping[key] # can't use 'key in mapping' with defaultdict 81 | except KeyError: 82 | pass 83 | return self.__missing__(key) # support subclasses that define __missing__ 84 | 85 | def get(self, key, default=None): 86 | return self[key] if key in self else default 87 | 88 | def __len__(self): 89 | return len(set().union(*self.maps)) # reuses stored hash values if possible 90 | 91 | def __iter__(self): 92 | return iter(set().union(*self.maps)) 93 | 94 | def __contains__(self, key): 95 | return any(key in m for m in self.maps) 96 | 97 | def __bool__(self): 98 | return any(self.maps) 99 | 100 | @_recursive_repr() 101 | def __repr__(self): 102 | return '{0.__class__.__name__}({1})'.format( 103 | self, ', '.join(map(repr, self.maps))) 104 | 105 | @classmethod 106 | def fromkeys(cls, iterable, *args): 107 | 'Create a ChainMap with a single dict created from the iterable.' 108 | return cls(dict.fromkeys(iterable, *args)) 109 | 110 | def copy(self): 111 | 'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]' 112 | return self.__class__(self.maps[0].copy(), *self.maps[1:]) 113 | 114 | __copy__ = copy 115 | 116 | def new_child(self, m=None): # like Django's _Context.push() 117 | ''' 118 | New ChainMap with a new map followed by all previous maps. If no 119 | map is provided, an empty dict is used. 120 | ''' 121 | if m is None: 122 | m = {} 123 | return self.__class__(m, *self.maps) 124 | 125 | @property 126 | def parents(self): # like Django's _Context.pop() 127 | 'New ChainMap from maps[1:].' 128 | return self.__class__(*self.maps[1:]) 129 | 130 | def __setitem__(self, key, value): 131 | self.maps[0][key] = value 132 | 133 | def __delitem__(self, key): 134 | try: 135 | del self.maps[0][key] 136 | except KeyError: 137 | raise KeyError('Key not found in the first mapping: {!r}'.format(key)) 138 | 139 | def popitem(self): 140 | 'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.' 141 | try: 142 | return self.maps[0].popitem() 143 | except KeyError: 144 | raise KeyError('No keys found in the first mapping.') 145 | 146 | def pop(self, key, *args): 147 | 'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].' 148 | try: 149 | return self.maps[0].pop(key, *args) 150 | except KeyError: 151 | raise KeyError('Key not found in the first mapping: {!r}'.format(key)) 152 | 153 | def clear(self): 154 | 'Clear maps[0], leaving maps[1:] intact.' 155 | self.maps[0].clear() 156 | -------------------------------------------------------------------------------- /src/pint/compat/lrucache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.compat.lrucache 4 | ~~~~~~~~~~~~~~~~~~~~ 5 | 6 | LRU (least recently used) cache backport. 7 | 8 | From https://code.activestate.com/recipes/578078-py26-and-py30-backport-of-python-33s-lru-cache/ 9 | 10 | :copyright: 2004, Raymond Hettinger, 11 | :license: MIT License 12 | """ 13 | 14 | from collections import namedtuple 15 | from functools import update_wrapper 16 | from threading import RLock 17 | 18 | _CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) 19 | 20 | class _HashedSeq(list): 21 | __slots__ = 'hashvalue' 22 | 23 | def __init__(self, tup, hash=hash): 24 | self[:] = tup 25 | self.hashvalue = hash(tup) 26 | 27 | def __hash__(self): 28 | return self.hashvalue 29 | 30 | def _make_key(args, kwds, typed, 31 | kwd_mark = (object(),), 32 | fasttypes = set((int, str, frozenset, type(None))), 33 | sorted=sorted, tuple=tuple, type=type, len=len): 34 | 'Make a cache key from optionally typed positional and keyword arguments' 35 | key = args 36 | if kwds: 37 | sorted_items = sorted(kwds.items()) 38 | key += kwd_mark 39 | for item in sorted_items: 40 | key += item 41 | if typed: 42 | key += tuple(type(v) for v in args) 43 | if kwds: 44 | key += tuple(type(v) for k, v in sorted_items) 45 | elif len(key) == 1 and type(key[0]) in fasttypes: 46 | return key[0] 47 | return _HashedSeq(key) 48 | 49 | def lru_cache(maxsize=100, typed=False): 50 | """Least-recently-used cache decorator. 51 | 52 | If *maxsize* is set to None, the LRU features are disabled and the cache 53 | can grow without bound. 54 | 55 | If *typed* is True, arguments of different types will be cached separately. 56 | For example, f(3.0) and f(3) will be treated as distinct calls with 57 | distinct results. 58 | 59 | Arguments to the cached function must be hashable. 60 | 61 | View the cache statistics named tuple (hits, misses, maxsize, currsize) with 62 | f.cache_info(). Clear the cache and statistics with f.cache_clear(). 63 | Access the underlying function with f.__wrapped__. 64 | 65 | See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used 66 | 67 | """ 68 | 69 | # Users should only access the lru_cache through its public API: 70 | # cache_info, cache_clear, and f.__wrapped__ 71 | # The internals of the lru_cache are encapsulated for thread safety and 72 | # to allow the implementation to change (including a possible C version). 73 | 74 | def decorating_function(user_function): 75 | 76 | cache = dict() 77 | stats = [0, 0] # make statistics updateable non-locally 78 | HITS, MISSES = 0, 1 # names for the stats fields 79 | make_key = _make_key 80 | cache_get = cache.get # bound method to lookup key or return None 81 | _len = len # localize the global len() function 82 | lock = RLock() # because linkedlist updates aren't threadsafe 83 | root = [] # root of the circular doubly linked list 84 | root[:] = [root, root, None, None] # initialize by pointing to self 85 | nonlocal_root = [root] # make updateable non-locally 86 | PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields 87 | 88 | if maxsize == 0: 89 | 90 | def wrapper(*args, **kwds): 91 | # no caching, just do a statistics update after a successful call 92 | result = user_function(*args, **kwds) 93 | stats[MISSES] += 1 94 | return result 95 | 96 | elif maxsize is None: 97 | 98 | def wrapper(*args, **kwds): 99 | # simple caching without ordering or size limit 100 | key = make_key(args, kwds, typed) 101 | result = cache_get(key, root) # root used here as a unique not-found sentinel 102 | if result is not root: 103 | stats[HITS] += 1 104 | return result 105 | result = user_function(*args, **kwds) 106 | cache[key] = result 107 | stats[MISSES] += 1 108 | return result 109 | 110 | else: 111 | 112 | def wrapper(*args, **kwds): 113 | # size limited caching that tracks accesses by recency 114 | key = make_key(args, kwds, typed) if kwds or typed else args 115 | with lock: 116 | link = cache_get(key) 117 | if link is not None: 118 | # record recent use of the key by moving it to the front of the list 119 | root, = nonlocal_root 120 | link_prev, link_next, key, result = link 121 | link_prev[NEXT] = link_next 122 | link_next[PREV] = link_prev 123 | last = root[PREV] 124 | last[NEXT] = root[PREV] = link 125 | link[PREV] = last 126 | link[NEXT] = root 127 | stats[HITS] += 1 128 | return result 129 | result = user_function(*args, **kwds) 130 | with lock: 131 | root, = nonlocal_root 132 | if key in cache: 133 | # getting here means that this same key was added to the 134 | # cache while the lock was released. since the link 135 | # update is already done, we need only return the 136 | # computed result and update the count of misses. 137 | pass 138 | elif _len(cache) >= maxsize: 139 | # use the old root to store the new key and result 140 | oldroot = root 141 | oldroot[KEY] = key 142 | oldroot[RESULT] = result 143 | # empty the oldest link and make it the new root 144 | root = nonlocal_root[0] = oldroot[NEXT] 145 | oldkey = root[KEY] 146 | oldvalue = root[RESULT] 147 | root[KEY] = root[RESULT] = None 148 | # now update the cache dictionary for the new links 149 | del cache[oldkey] 150 | cache[key] = oldroot 151 | else: 152 | # put result in a new link at the front of the list 153 | last = root[PREV] 154 | link = [last, root, key, result] 155 | last[NEXT] = root[PREV] = cache[key] = link 156 | stats[MISSES] += 1 157 | return result 158 | 159 | def cache_info(): 160 | """Report cache statistics""" 161 | with lock: 162 | return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) 163 | 164 | def cache_clear(): 165 | """Clear the cache and cache statistics""" 166 | with lock: 167 | cache.clear() 168 | root = nonlocal_root[0] 169 | root[:] = [root, root, None, None] 170 | stats[:] = [0, 0] 171 | 172 | wrapper.__wrapped__ = user_function 173 | wrapper.cache_info = cache_info 174 | wrapper.cache_clear = cache_clear 175 | return update_wrapper(wrapper, user_function) 176 | 177 | return decorating_function 178 | -------------------------------------------------------------------------------- /src/pint/compat/meta.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.compat.meta 4 | ~~~~~~~~~~~~~~~~ 5 | 6 | Compatibility layer. 7 | 8 | :copyright: 2016 by Pint Authors, see AUTHORS for more details. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | 13 | def with_metaclass(meta, *bases): 14 | """Create a base class with a metaclass.""" 15 | # This requires a bit of explanation: the basic idea is to make a dummy 16 | # metaclass for one level of class instantiation that replaces itself with 17 | # the actual metaclass. 18 | 19 | # Taken from six 20 | 21 | class metaclass(meta): 22 | 23 | def __new__(cls, name, this_bases, d): 24 | return meta(name, bases, d) 25 | return type.__new__(metaclass, 'temporary_class', (), {}) -------------------------------------------------------------------------------- /src/pint/constants_en.txt: -------------------------------------------------------------------------------- 1 | # Default Pint constants definition file 2 | # Based on the International System of Units 3 | # Language: english 4 | # Source: http://physics.nist.gov/cuu/Constants/Table/allascii.txt 5 | # :copyright: 2013 by Pint Authors, see AUTHORS for more details. 6 | 7 | speed_of_light = 299792458 * meter / second = c 8 | standard_gravity = 9.806650 * meter / second ** 2 = g_0 = g_n = gravity 9 | vacuum_permeability = 4 * pi * 1e-7 * newton / ampere ** 2 = mu_0 = magnetic_constant 10 | vacuum_permittivity = 1 / (mu_0 * c **2 ) = epsilon_0 = electric_constant 11 | Z_0 = mu_0 * c = impedance_of_free_space = characteristic_impedance_of_vacuum 12 | 13 | # 0.000 000 29 e-34 14 | planck_constant = 6.62606957e-34 J s = h 15 | hbar = planck_constant / (2 * pi) = ħ 16 | 17 | # 0.000 80 e-11 18 | newtonian_constant_of_gravitation = 6.67384e-11 m^3 kg^-1 s^-2 19 | 20 | # 0.000 000 035 e-19 21 | # elementary_charge = 1.602176565e-19 C = e 22 | 23 | # 0.000 0075 24 | molar_gas_constant = 8.3144621 J mol^-1 K^-1 = R 25 | 26 | # 0.000 000 0024 e-3 27 | fine_structure_constant = 7.2973525698e-3 28 | 29 | # 0.000 000 27 e23 30 | avogadro_number = 6.02214129e23 mol^-1 =N_A 31 | 32 | # 0.000 0013 e-23 33 | boltzmann_constant = 1.3806488e-23 J K^-1 = k 34 | 35 | # 0.000 021 e-8 36 | stefan_boltzmann_constant = 5.670373e-8 W m^-2 K^-4 = σ 37 | 38 | # 0.000 0053 e10 39 | wien_frequency_displacement_law_constant = 5.8789254e10 Hz K^-1 40 | 41 | # 0.000 055 42 | rydberg_constant = 10973731.568539 m^-1 43 | 44 | # 0.000 000 40 e-31 45 | electron_mass = 9.10938291e-31 kg = m_e 46 | 47 | # 0.000 000 074 e-27 48 | neutron_mass = 1.674927351e-27 kg = m_n 49 | 50 | # 0.000 000 074 e-27 51 | proton_mass = 1.672621777e-27 kg = m_p 52 | -------------------------------------------------------------------------------- /src/pint/context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.context 4 | ~~~~~~~~~~~~ 5 | 6 | Functions and classes related to context definitions and application. 7 | 8 | :copyright: 2016 by Pint Authors, see AUTHORS for more details.. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | from __future__ import division, unicode_literals, print_function, absolute_import 13 | 14 | 15 | import re 16 | from collections import defaultdict 17 | import weakref 18 | 19 | from .compat import ChainMap 20 | from .util import (ParserHelper, UnitsContainer, string_types, 21 | to_units_container, SourceIterator) 22 | from .errors import DefinitionSyntaxError 23 | 24 | #: Regex to match the header parts of a context. 25 | _header_re = re.compile('@context\s*(?P\(.*\))?\s+(?P\w+)\s*(=(?P.*))*') 26 | 27 | #: Regex to match variable names in an equation. 28 | _varname_re = re.compile('[A-Za-z_][A-Za-z0-9_]*') 29 | 30 | 31 | def _expression_to_function(eq): 32 | def func(ureg, value, **kwargs): 33 | return ureg.parse_expression(eq, value=value, **kwargs) 34 | return func 35 | 36 | 37 | class Context(object): 38 | """A specialized container that defines transformation functions from one 39 | dimension to another. Each Dimension are specified using a UnitsContainer. 40 | Simple transformation are given with a function taking a single parameter. 41 | 42 | >>> timedim = UnitsContainer({'[time]': 1}) 43 | >>> spacedim = UnitsContainer({'[length]': 1}) 44 | >>> def f(time): 45 | ... 'Time to length converter' 46 | ... return 3. * time 47 | >>> c = Context() 48 | >>> c.add_transformation(timedim, spacedim, f) 49 | >>> c.transform(timedim, spacedim, 2) 50 | 6 51 | 52 | Conversion functions may take optional keyword arguments and the context 53 | can have default values for these arguments. 54 | 55 | >>> def f(time, n): 56 | ... 'Time to length converter, n is the index of refraction of the material' 57 | ... return 3. * time / n 58 | >>> c = Context(n=3) 59 | >>> c.add_transformation(timedim, spacedim, f) 60 | >>> c.transform(timedim, spacedim, 2) 61 | 2 62 | 63 | """ 64 | 65 | def __init__(self, name, aliases=(), defaults=None): 66 | 67 | self.name = name 68 | self.aliases = aliases 69 | 70 | #: Maps (src, dst) -> transformation function 71 | self.funcs = {} 72 | 73 | #: Maps defaults variable names to values 74 | self.defaults = defaults or {} 75 | 76 | #: Maps (src, dst) -> self 77 | #: Used as a convenience dictionary to be composed by ContextChain 78 | self.relation_to_context = weakref.WeakValueDictionary() 79 | 80 | @classmethod 81 | def from_context(cls, context, **defaults): 82 | """Creates a new context that shares the funcs dictionary with the 83 | original context. The default values are copied from the original 84 | context and updated with the new defaults. 85 | 86 | If defaults is empty, return the same context. 87 | """ 88 | if defaults: 89 | newdef = dict(context.defaults, **defaults) 90 | c = cls(context.name, context.aliases, newdef) 91 | c.funcs = context.funcs 92 | for edge in context.funcs.keys(): 93 | c.relation_to_context[edge] = c 94 | return c 95 | return context 96 | 97 | @classmethod 98 | def from_lines(cls, lines, to_base_func=None): 99 | lines = SourceIterator(lines) 100 | 101 | lineno, header = next(lines) 102 | try: 103 | r = _header_re.search(header) 104 | name = r.groupdict()['name'].strip() 105 | aliases = r.groupdict()['aliases'] 106 | if aliases: 107 | aliases = tuple(a.strip() for a in r.groupdict()['aliases'].split('=')) 108 | else: 109 | aliases = () 110 | defaults = r.groupdict()['defaults'] 111 | except: 112 | raise DefinitionSyntaxError("Could not parse the Context header '%s'" % header, 113 | lineno=lineno) 114 | 115 | if defaults: 116 | def to_num(val): 117 | val = complex(val) 118 | if not val.imag: 119 | return val.real 120 | return val 121 | 122 | _txt = defaults 123 | try: 124 | defaults = (part.split('=') for part in defaults.strip('()').split(',')) 125 | defaults = dict((str(k).strip(), to_num(v)) 126 | for k, v in defaults) 127 | except (ValueError, TypeError): 128 | raise DefinitionSyntaxError("Could not parse Context definition defaults: '%s'", _txt, 129 | lineno=lineno) 130 | 131 | ctx = cls(name, aliases, defaults) 132 | else: 133 | ctx = cls(name, aliases) 134 | 135 | names = set() 136 | for lineno, line in lines: 137 | try: 138 | rel, eq = line.split(':') 139 | names.update(_varname_re.findall(eq)) 140 | 141 | func = _expression_to_function(eq) 142 | 143 | if '<->' in rel: 144 | src, dst = (ParserHelper.from_string(s) 145 | for s in rel.split('<->')) 146 | if to_base_func: 147 | src = to_base_func(src) 148 | dst = to_base_func(dst) 149 | ctx.add_transformation(src, dst, func) 150 | ctx.add_transformation(dst, src, func) 151 | elif '->' in rel: 152 | src, dst = (ParserHelper.from_string(s) 153 | for s in rel.split('->')) 154 | if to_base_func: 155 | src = to_base_func(src) 156 | dst = to_base_func(dst) 157 | ctx.add_transformation(src, dst, func) 158 | else: 159 | raise Exception 160 | except: 161 | raise DefinitionSyntaxError("Could not parse Context %s relation '%s'" % (name, line), 162 | lineno=lineno) 163 | 164 | if defaults: 165 | missing_pars = set(defaults.keys()).difference(set(names)) 166 | if missing_pars: 167 | raise DefinitionSyntaxError('Context parameters {} not found in any equation.'.format(missing_pars)) 168 | 169 | return ctx 170 | 171 | def add_transformation(self, src, dst, func): 172 | """Add a transformation function to the context. 173 | """ 174 | _key = self.__keytransform__(src, dst) 175 | self.funcs[_key] = func 176 | self.relation_to_context[_key] = self 177 | 178 | def remove_transformation(self, src, dst): 179 | """Add a transformation function to the context. 180 | """ 181 | _key = self.__keytransform__(src, dst) 182 | del self.funcs[_key] 183 | del self.relation_to_context[_key] 184 | 185 | @staticmethod 186 | def __keytransform__(src, dst): 187 | return to_units_container(src), to_units_container(dst) 188 | 189 | def transform(self, src, dst, registry, value): 190 | """Transform a value. 191 | """ 192 | _key = self.__keytransform__(src, dst) 193 | return self.funcs[_key](registry, value, **self.defaults) 194 | 195 | 196 | class ContextChain(ChainMap): 197 | """A specialized ChainMap for contexts that simplifies finding rules 198 | to transform from one dimension to another. 199 | """ 200 | 201 | def __init__(self, *args, **kwargs): 202 | super(ContextChain, self).__init__(*args, **kwargs) 203 | self._graph = None 204 | self._contexts = [] 205 | 206 | def insert_contexts(self, *contexts): 207 | """Insert one or more contexts in reversed order the chained map. 208 | (A rule in last context will take precedence) 209 | 210 | To facilitate the identification of the context with the matching rule, 211 | the *relation_to_context* dictionary of the context is used. 212 | """ 213 | self._contexts.insert(0, contexts) 214 | self.maps = [ctx.relation_to_context for ctx in reversed(contexts)] + self.maps 215 | self._graph = None 216 | 217 | def remove_contexts(self, n): 218 | """Remove the last n inserted contexts from the chain. 219 | """ 220 | self._contexts = self._contexts[n:] 221 | self.maps = self.maps[n:] 222 | self._graph = None 223 | 224 | @property 225 | def defaults(self): 226 | if self: 227 | return list(self.maps[0].values())[0].defaults 228 | return {} 229 | 230 | @property 231 | def graph(self): 232 | """The graph relating 233 | """ 234 | if self._graph is None: 235 | self._graph = defaultdict(set) 236 | for fr_, to_ in self: 237 | self._graph[fr_].add(to_) 238 | return self._graph 239 | 240 | def transform(self, src, dst, registry, value): 241 | """Transform the value, finding the rule in the chained context. 242 | (A rule in last context will take precedence) 243 | 244 | :raises: KeyError if the rule is not found. 245 | """ 246 | return self[(src, dst)].transform(src, dst, registry, value) 247 | -------------------------------------------------------------------------------- /src/pint/converters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.converters 4 | ~~~~~~~~~ 5 | 6 | Functions and classes related to unit conversions. 7 | 8 | :copyright: 2016 by Pint Authors, see AUTHORS for more details. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | from __future__ import (division, unicode_literals, print_function, 12 | absolute_import) 13 | 14 | 15 | class Converter(object): 16 | """Base class for value converters. 17 | """ 18 | 19 | is_multiplicative = True 20 | 21 | def to_reference(self, value, inplace=False): 22 | return value 23 | 24 | def from_reference(self, value, inplace=False): 25 | return value 26 | 27 | 28 | class ScaleConverter(Converter): 29 | """A linear transformation 30 | """ 31 | 32 | is_multiplicative = True 33 | 34 | def __init__(self, scale): 35 | self.scale = scale 36 | 37 | def to_reference(self, value, inplace=False): 38 | if inplace: 39 | value *= self.scale 40 | else: 41 | value = value * self.scale 42 | 43 | return value 44 | 45 | def from_reference(self, value, inplace=False): 46 | if inplace: 47 | value /= self.scale 48 | else: 49 | value = value / self.scale 50 | 51 | return value 52 | 53 | 54 | class OffsetConverter(Converter): 55 | """An affine transformation 56 | """ 57 | 58 | def __init__(self, scale, offset): 59 | self.scale = scale 60 | self.offset = offset 61 | 62 | @property 63 | def is_multiplicative(self): 64 | return self.offset == 0 65 | 66 | def to_reference(self, value, inplace=False): 67 | if inplace: 68 | value *= self.scale 69 | value += self.offset 70 | else: 71 | value = value * self.scale + self.offset 72 | 73 | return value 74 | 75 | def from_reference(self, value, inplace=False): 76 | if inplace: 77 | value -= self.offset 78 | value /= self.scale 79 | else: 80 | value = (value - self.offset) / self.scale 81 | 82 | return value 83 | -------------------------------------------------------------------------------- /src/pint/definitions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.definitions 4 | ~~~~~~~~~ 5 | 6 | Functions and classes related to unit definitions. 7 | 8 | :copyright: 2016 by Pint Authors, see AUTHORS for more details. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | from __future__ import (division, unicode_literals, print_function, 13 | absolute_import) 14 | 15 | from .converters import ScaleConverter, OffsetConverter 16 | from .util import UnitsContainer, _is_dim, ParserHelper 17 | from .compat import string_types 18 | 19 | 20 | class Definition(object): 21 | """Base class for definitions. 22 | 23 | :param name: name. 24 | :param symbol: a short name or symbol for the definition 25 | :param aliases: iterable of other names. 26 | :param converter: an instance of Converter. 27 | """ 28 | 29 | def __init__(self, name, symbol, aliases, converter): 30 | self._name = name 31 | self._symbol = symbol 32 | self._aliases = aliases 33 | self._converter = converter 34 | 35 | @property 36 | def is_multiplicative(self): 37 | return self._converter.is_multiplicative 38 | 39 | @classmethod 40 | def from_string(cls, definition): 41 | """Parse a definition 42 | """ 43 | name, definition = definition.split('=', 1) 44 | name = name.strip() 45 | 46 | result = [res.strip() for res in definition.split('=')] 47 | value, aliases = result[0], tuple(result[1:]) 48 | symbol, aliases = (aliases[0], aliases[1:]) if aliases else (None, 49 | aliases) 50 | 51 | if name.startswith('['): 52 | return DimensionDefinition(name, symbol, aliases, value) 53 | elif name.endswith('-'): 54 | name = name.rstrip('-') 55 | return PrefixDefinition(name, symbol, aliases, value) 56 | else: 57 | return UnitDefinition(name, symbol, aliases, value) 58 | 59 | @property 60 | def name(self): 61 | return self._name 62 | 63 | @property 64 | def symbol(self): 65 | return self._symbol or self._name 66 | 67 | @property 68 | def has_symbol(self): 69 | return bool(self._symbol) 70 | 71 | @property 72 | def aliases(self): 73 | return self._aliases 74 | 75 | @property 76 | def converter(self): 77 | return self._converter 78 | 79 | def __str__(self): 80 | return self.name 81 | 82 | 83 | class PrefixDefinition(Definition): 84 | """Definition of a prefix. 85 | """ 86 | 87 | def __init__(self, name, symbol, aliases, converter): 88 | if isinstance(converter, string_types): 89 | converter = ScaleConverter(eval(converter)) 90 | aliases = tuple(alias.strip('-') for alias in aliases) 91 | if symbol: 92 | symbol = symbol.strip('-') 93 | super(PrefixDefinition, self).__init__(name, symbol, aliases, 94 | converter) 95 | 96 | 97 | class UnitDefinition(Definition): 98 | """Definition of a unit. 99 | 100 | :param reference: Units container with reference units. 101 | :param is_base: indicates if it is a base unit. 102 | """ 103 | 104 | def __init__(self, name, symbol, aliases, converter, 105 | reference=None, is_base=False): 106 | self.reference = reference 107 | self.is_base = is_base 108 | if isinstance(converter, string_types): 109 | if ';' in converter: 110 | [converter, modifiers] = converter.split(';', 2) 111 | modifiers = dict((key.strip(), eval(value)) for key, value in 112 | (part.split(':') 113 | for part in modifiers.split(';'))) 114 | else: 115 | modifiers = {} 116 | 117 | converter = ParserHelper.from_string(converter) 118 | if all(_is_dim(key) for key in converter.keys()): 119 | self.is_base = True 120 | elif not any(_is_dim(key) for key in converter.keys()): 121 | self.is_base = False 122 | else: 123 | raise ValueError('Cannot mix dimensions and units in the same definition. ' 124 | 'Base units must be referenced only to dimensions. ' 125 | 'Derived units must be referenced only to units.') 126 | self.reference = UnitsContainer(converter) 127 | if modifiers.get('offset', 0.) != 0.: 128 | converter = OffsetConverter(converter.scale, 129 | modifiers['offset']) 130 | else: 131 | converter = ScaleConverter(converter.scale) 132 | 133 | super(UnitDefinition, self).__init__(name, symbol, aliases, converter) 134 | 135 | 136 | class DimensionDefinition(Definition): 137 | """Definition of a dimension. 138 | """ 139 | 140 | def __init__(self, name, symbol, aliases, converter, 141 | reference=None, is_base=False): 142 | self.reference = reference 143 | self.is_base = is_base 144 | if isinstance(converter, string_types): 145 | converter = ParserHelper.from_string(converter) 146 | if not converter: 147 | self.is_base = True 148 | elif all(_is_dim(key) for key in converter.keys()): 149 | self.is_base = False 150 | else: 151 | raise ValueError('Base dimensions must be referenced to None. ' 152 | 'Derived dimensions must only be referenced ' 153 | 'to dimensions.') 154 | self.reference = UnitsContainer(converter) 155 | 156 | super(DimensionDefinition, self).__init__(name, symbol, aliases, 157 | converter=None) 158 | -------------------------------------------------------------------------------- /src/pint/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.errors 4 | ~~~~~~~~~ 5 | 6 | Functions and classes related to unit definitions and conversions. 7 | 8 | :copyright: 2016 by Pint Authors, see AUTHORS for more details. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | from __future__ import (division, unicode_literals, print_function, 12 | absolute_import) 13 | 14 | from .compat import string_types 15 | 16 | 17 | class DefinitionSyntaxError(ValueError): 18 | """Raised when a textual definition has a syntax error. 19 | """ 20 | 21 | def __init__(self, msg, filename=None, lineno=None): 22 | super(DefinitionSyntaxError, self).__init__() 23 | self.msg = msg 24 | self.filename = None 25 | self.lineno = None 26 | 27 | def __str__(self): 28 | mess = "While opening {}, in line {}: " 29 | return mess.format(self.filename, self.lineno) + self.msg 30 | 31 | 32 | class RedefinitionError(ValueError): 33 | """Raised when a unit or prefix is redefined. 34 | """ 35 | 36 | def __init__(self, name, definition_type): 37 | super(RedefinitionError, self).__init__() 38 | self.name = name 39 | self.definition_type = definition_type 40 | self.filename = None 41 | self.lineno = None 42 | 43 | def __str__(self): 44 | msg = "cannot redefine '{}' ({})".format(self.name, 45 | self.definition_type) 46 | if self.filename: 47 | mess = "While opening {}, in line {}: " 48 | return mess.format(self.filename, self.lineno) + msg 49 | return msg 50 | 51 | 52 | class UndefinedUnitError(AttributeError): 53 | """Raised when the units are not defined in the unit registry. 54 | """ 55 | 56 | def __init__(self, unit_names): 57 | super(UndefinedUnitError, self).__init__() 58 | self.unit_names = unit_names 59 | 60 | def __str__(self): 61 | mess = "'{}' is not defined in the unit registry" 62 | mess_plural = "'{}' are not defined in the unit registry" 63 | if isinstance(self.unit_names, string_types): 64 | return mess.format(self.unit_names) 65 | elif isinstance(self.unit_names, (list, tuple))\ 66 | and len(self.unit_names) == 1: 67 | return mess.format(self.unit_names[0]) 68 | elif isinstance(self.unit_names, set) and len(self.unit_names) == 1: 69 | uname = list(self.unit_names)[0] 70 | return mess.format(uname) 71 | else: 72 | return mess_plural.format(self.unit_names) 73 | 74 | 75 | class DimensionalityError(ValueError): 76 | """Raised when trying to convert between incompatible units. 77 | """ 78 | 79 | def __init__(self, units1, units2, dim1=None, dim2=None, extra_msg=''): 80 | super(DimensionalityError, self).__init__() 81 | self.units1 = units1 82 | self.units2 = units2 83 | self.dim1 = dim1 84 | self.dim2 = dim2 85 | self.extra_msg = extra_msg 86 | 87 | def __str__(self): 88 | if self.dim1 or self.dim2: 89 | dim1 = ' ({})'.format(self.dim1) 90 | dim2 = ' ({})'.format(self.dim2) 91 | else: 92 | dim1 = '' 93 | dim2 = '' 94 | 95 | msg = "Cannot convert from '{}'{} to '{}'{}" + self.extra_msg 96 | 97 | return msg.format(self.units1, dim1, self.units2, dim2) 98 | 99 | 100 | class OffsetUnitCalculusError(ValueError): 101 | """Raised on ambiguous operations with offset units. 102 | """ 103 | def __init__(self, units1, units2='', extra_msg=''): 104 | super(ValueError, self).__init__() 105 | self.units1 = units1 106 | self.units2 = units2 107 | self.extra_msg = extra_msg 108 | 109 | def __str__(self): 110 | msg = ("Ambiguous operation with offset unit (%s)." % 111 | ', '.join(['%s' % u for u in [self.units1, self.units2] if u]) 112 | + self.extra_msg) 113 | return msg.format(self.units1, self.units2) 114 | 115 | 116 | class UnitStrippedWarning(UserWarning): 117 | pass 118 | -------------------------------------------------------------------------------- /src/pint/matplotlib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.matplotlib 4 | ~~~~~~~~~ 5 | 6 | Functions and classes related to working with Matplotlib's support 7 | for plotting with units. 8 | 9 | :copyright: 2017 by Pint Authors, see AUTHORS for more details. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | import matplotlib.units 13 | 14 | 15 | class PintAxisInfo(matplotlib.units.AxisInfo): 16 | """Support default axis and tick labeling and default limits.""" 17 | 18 | def __init__(self, units): 19 | """Set the default label to the pretty-print of the unit.""" 20 | super(PintAxisInfo, self).__init__(label='{:P}'.format(units)) 21 | 22 | 23 | class PintConverter(matplotlib.units.ConversionInterface): 24 | """Implement support for pint within matplotlib's unit conversion framework.""" 25 | 26 | def __init__(self, registry): 27 | super(PintConverter, self).__init__() 28 | self._reg = registry 29 | 30 | def convert(self, value, unit, axis): 31 | """Convert :`Quantity` instances for matplotlib to use.""" 32 | if isinstance(value, (tuple, list)): 33 | return [self._convert_value(v, unit, axis) for v in value] 34 | else: 35 | return self._convert_value(value, unit, axis) 36 | 37 | def _convert_value(self, value, unit, axis): 38 | """Handle converting using attached unit or falling back to axis units.""" 39 | if hasattr(value, 'units'): 40 | return value.to(unit).magnitude 41 | else: 42 | return self._reg.Quantity(value, axis.get_units()).to(unit).magnitude 43 | 44 | @staticmethod 45 | def axisinfo(unit, axis): 46 | """Return axis information for this particular unit.""" 47 | return PintAxisInfo(unit) 48 | 49 | @staticmethod 50 | def default_units(x, axis): 51 | """Get the default unit to use for the given combination of unit and axis.""" 52 | return getattr(x, 'units', None) 53 | 54 | 55 | def setup_matplotlib_handlers(registry, enable): 56 | """Set up matplotlib's unit support to handle units from a registry. 57 | :param registry: the registry that will be used 58 | :type registry: UnitRegistry 59 | :param enable: whether support should be enabled or disabled 60 | :type enable: bool 61 | """ 62 | if matplotlib.__version__ < '2.0': 63 | raise RuntimeError('Matplotlib >= 2.0 required to work with pint.') 64 | 65 | if enable: 66 | matplotlib.units.registry[registry.Quantity] = PintConverter(registry) 67 | else: 68 | matplotlib.units.registry.pop(registry.Quantity, None) 69 | -------------------------------------------------------------------------------- /src/pint/measurement.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.measurement 4 | ~~~~~~~~~~~~~~~~ 5 | 6 | :copyright: 2016 by Pint Authors, see AUTHORS for more details. 7 | :license: BSD, see LICENSE for more details. 8 | """ 9 | 10 | from __future__ import division, unicode_literals, print_function, absolute_import 11 | 12 | from .compat import ufloat 13 | from .formatting import _FORMATS, siunitx_format_unit 14 | 15 | MISSING = object() 16 | 17 | 18 | class _Measurement(object): 19 | """Implements a class to describe a quantity with uncertainty. 20 | 21 | :param value: The most likely value of the measurement. 22 | :type value: Quantity or Number 23 | :param error: The error or uncertainty of the measurement. 24 | :type error: Quantity or Number 25 | """ 26 | 27 | def __new__(cls, value, error, units=MISSING): 28 | if units is MISSING: 29 | try: 30 | value, units = value.magnitude, value.units 31 | except AttributeError: 32 | #if called with two arguments and the first looks like a ufloat 33 | # then assume the second argument is the units, keep value intact 34 | if hasattr(value,"nominal_value"): 35 | units = error 36 | error = MISSING #used for check below 37 | else: 38 | units = '' 39 | try: 40 | error = error.to(units).magnitude 41 | except AttributeError: 42 | pass 43 | 44 | if error is MISSING: 45 | mag = value 46 | elif error < 0: 47 | raise ValueError('The magnitude of the error cannot be negative'.format(value, error)) 48 | else: 49 | mag = ufloat(value,error) 50 | 51 | inst = super(_Measurement, cls).__new__(cls, mag, units) 52 | return inst 53 | 54 | @property 55 | def value(self): 56 | return self._REGISTRY.Quantity(self.magnitude.nominal_value, self.units) 57 | 58 | @property 59 | def error(self): 60 | return self._REGISTRY.Quantity(self.magnitude.std_dev, self.units) 61 | 62 | @property 63 | def rel(self): 64 | return float(abs(self.magnitude.std_dev / self.magnitude.nominal_value)) 65 | 66 | def __repr__(self): 67 | return "".format(self.magnitude.nominal_value, 68 | self.magnitude.std_dev, 69 | self.units) 70 | 71 | def __str__(self): 72 | return '{0}'.format(self) 73 | 74 | def __format__(self, spec): 75 | # special cases 76 | if 'Lx' in spec: # the LaTeX siunitx code 77 | # the uncertainties module supports formatting 78 | # numbers in value(unc) notation (i.e. 1.23(45) instead of 1.23 +/- 0.45), 79 | # which siunitx actually accepts as input. we just need to give the 'S' 80 | # formatting option for the uncertainties module. 81 | spec = spec.replace('Lx','S') 82 | # todo: add support for extracting options 83 | opts = 'separate-uncertainty=true' 84 | mstr = format( self.magnitude, spec ) 85 | ustr = siunitx_format_unit(self.units) 86 | ret = r'\SI[%s]{%s}{%s}'%( opts, mstr, ustr ) 87 | return ret 88 | 89 | 90 | # standard cases 91 | if 'L' in spec: 92 | newpm = pm = r' \pm ' 93 | pars = _FORMATS['L']['parentheses_fmt'] 94 | elif 'P' in spec: 95 | newpm = pm = '±' 96 | pars = _FORMATS['P']['parentheses_fmt'] 97 | else: 98 | newpm = pm = '+/-' 99 | pars = _FORMATS['']['parentheses_fmt'] 100 | 101 | if 'C' in spec: 102 | sp = '' 103 | newspec = spec.replace('C', '') 104 | pars = _FORMATS['C']['parentheses_fmt'] 105 | else: 106 | sp = ' ' 107 | newspec = spec 108 | 109 | if 'H' in spec: 110 | newpm = '±' 111 | newspec = spec.replace('H', '') 112 | pars = _FORMATS['H']['parentheses_fmt'] 113 | 114 | mag = format(self.magnitude, newspec).replace(pm, sp + newpm + sp) 115 | 116 | if 'L' in newspec and 'S' in newspec: 117 | mag = mag.replace('(', r'\left(').replace(')', r'\right)') 118 | 119 | if 'L' in newspec: 120 | space = r'\ ' 121 | else: 122 | space = ' ' 123 | 124 | if 'uS' in newspec or 'ue' in newspec or 'u%' in newspec: 125 | return mag + space + format(self.units, spec) 126 | else: 127 | return pars.format(mag) + space + format(self.units, spec) 128 | 129 | 130 | def build_measurement_class(registry, force_ndarray=False): 131 | 132 | if ufloat is None: 133 | class Measurement(object): 134 | 135 | def __init__(self, *args): 136 | raise RuntimeError("Pint requires the 'uncertainties' package to create a Measurement object.") 137 | 138 | else: 139 | class Measurement(_Measurement, registry.Quantity): 140 | pass 141 | 142 | Measurement._REGISTRY = registry 143 | Measurement.force_ndarray = force_ndarray 144 | 145 | return Measurement 146 | -------------------------------------------------------------------------------- /src/pint/pint_eval.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.pint_eval 4 | ~~~~~~~~~~~~~~ 5 | 6 | An expression evaluator to be used as a safe replacement for builtin eval. 7 | 8 | :copyright: 2016 by Pint Authors, see AUTHORS for more details. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | from decimal import Decimal 13 | import math 14 | import operator 15 | 16 | import token as tokenlib 17 | 18 | from .errors import DefinitionSyntaxError 19 | 20 | # For controlling order of operations 21 | _OP_PRIORITY = { 22 | '**': 3, 23 | '^': 3, 24 | 'unary': 2, 25 | '*': 1, 26 | '': 1, # operator for implicit ops 27 | '/': 1, 28 | '+': 0, 29 | '-' : 0 30 | } 31 | 32 | _BINARY_OPERATOR_MAP = { 33 | '**': operator.pow, 34 | '*': operator.mul, 35 | '': operator.mul, # operator for implicit ops 36 | '/': operator.truediv, 37 | '+': operator.add, 38 | '-': operator.sub 39 | } 40 | 41 | _UNARY_OPERATOR_MAP = { 42 | '+': lambda x: x, 43 | '-': lambda x: x * -1 44 | } 45 | 46 | 47 | class EvalTreeNode(object): 48 | 49 | def __init__(self, left, operator=None, right=None): 50 | """ 51 | left + operator + right --> binary op 52 | left + operator --> unary op 53 | left + right --> implicit op 54 | left --> single value 55 | """ 56 | self.left = left 57 | self.operator = operator 58 | self.right = right 59 | 60 | def to_string(self): 61 | # For debugging purposes 62 | if self.right: 63 | comps = [self.left.to_string()] 64 | if self.operator: 65 | comps.append(self.operator[1]) 66 | comps.append(self.right.to_string()) 67 | elif self.operator: 68 | comps = [self.operator[1], self.left.to_string()] 69 | else: 70 | return self.left[1] 71 | return '(%s)' % ' '.join(comps) 72 | 73 | def evaluate(self, define_op, bin_op=_BINARY_OPERATOR_MAP, un_op=_UNARY_OPERATOR_MAP): 74 | """ 75 | define_op is a callable that translates tokens into objects 76 | bin_op and un_op provide functions for performing binary and unary operations 77 | """ 78 | 79 | if self.right: 80 | # binary or implicit operator 81 | op_text = self.operator[1] if self.operator else '' 82 | if op_text not in bin_op: 83 | raise DefinitionSyntaxError('missing binary operator "%s"' % op_text) 84 | left = self.left.evaluate(define_op, bin_op, un_op) 85 | return bin_op[op_text](left, self.right.evaluate(define_op, bin_op, un_op)) 86 | elif self.operator: 87 | # unary operator 88 | op_text = self.operator[1] 89 | if op_text not in un_op: 90 | raise DefinitionSyntaxError('missing unary operator "%s"' % op_text) 91 | return un_op[op_text](self.left.evaluate(define_op, bin_op, un_op)) 92 | else: 93 | # single value 94 | return define_op(self.left) 95 | 96 | 97 | def build_eval_tree(tokens, op_priority=_OP_PRIORITY, index=0, depth=0, prev_op=None, ): 98 | """ 99 | Params: 100 | Index, depth, and prev_op used recursively, so don't touch. 101 | Tokens is an iterable of tokens from an expression to be evaluated. 102 | 103 | Transform the tokens from an expression into a recursive parse tree, following order of operations. 104 | Operations can include binary ops (3 + 4), implicit ops (3 kg), or unary ops (-1). 105 | 106 | General Strategy: 107 | 1) Get left side of operator 108 | 2) If no tokens left, return final result 109 | 3) Get operator 110 | 4) Use recursion to create tree starting at token on right side of operator (start at step #1) 111 | 4.1) If recursive call encounters an operator with lower or equal priority to step #2, exit recursion 112 | 5) Combine left side, operator, and right side into a new left side 113 | 6) Go back to step #2 114 | """ 115 | 116 | if depth == 0 and prev_op == None: 117 | # ensure tokens is list so we can access by index 118 | tokens = list(tokens) 119 | 120 | result = None 121 | 122 | while True: 123 | current_token = tokens[index] 124 | token_type = current_token[0] 125 | token_text = current_token[1] 126 | 127 | if token_type == tokenlib.OP: 128 | if token_text == ')': 129 | if prev_op is None: 130 | raise DefinitionSyntaxError('unopened parentheses in tokens: %s' % current_token) 131 | elif prev_op == '(': 132 | # close parenthetical group 133 | return result, index 134 | else: 135 | # parenthetical group ending, but we need to close sub-operations within group 136 | return result, index - 1 137 | elif token_text == '(': 138 | # gather parenthetical group 139 | right, index = build_eval_tree(tokens, op_priority, index+1, 0, token_text) 140 | if not tokens[index][1] == ')': 141 | raise DefinitionSyntaxError('weird exit from parentheses') 142 | if result: 143 | # implicit op with a parenthetical group, i.e. "3 (kg ** 2)" 144 | result = EvalTreeNode(left=result, right=right) 145 | else: 146 | # get first token 147 | result = right 148 | elif token_text in op_priority: 149 | if result: 150 | # equal-priority operators are grouped in a left-to-right order, unless they're 151 | # exponentiation, in which case they're grouped right-to-left 152 | # this allows us to get the expected behavior for multiple exponents 153 | # (2^3^4) --> (2^(3^4)) 154 | # (2 * 3 / 4) --> ((2 * 3) / 4) 155 | if op_priority[token_text] <= op_priority.get(prev_op, -1) and token_text not in ['**', '^']: 156 | # previous operator is higher priority, so end previous binary op 157 | return result, index - 1 158 | # get right side of binary op 159 | right, index = build_eval_tree(tokens, op_priority, index+1, depth+1, token_text) 160 | result = EvalTreeNode(left=result, operator=current_token, right=right) 161 | else: 162 | # unary operator 163 | right, index = build_eval_tree(tokens, op_priority, index+1, depth+1, 'unary') 164 | result = EvalTreeNode(left=right, operator=current_token) 165 | elif token_type == tokenlib.NUMBER or token_type == tokenlib.NAME: 166 | if result: 167 | # tokens with an implicit operation i.e. "1 kg" 168 | if op_priority[''] <= op_priority.get(prev_op, -1): 169 | # previous operator is higher priority than implicit, so end previous binary op 170 | return result, index - 1 171 | right, index = build_eval_tree(tokens, op_priority, index, depth+1, '') 172 | result = EvalTreeNode(left=result, right=right) 173 | else: 174 | # get first token 175 | result = EvalTreeNode(left=current_token) 176 | 177 | if tokens[index][0] == tokenlib.ENDMARKER: 178 | if prev_op == '(': 179 | raise DefinitionSyntaxError('unclosed parentheses in tokens') 180 | if depth > 0 or prev_op: 181 | # have to close recursion 182 | return result, index 183 | else: 184 | # recursion all closed, so just return the final result 185 | return result 186 | 187 | if index + 1 >= len(tokens): 188 | # should hit ENDMARKER before this ever happens 189 | raise DefinitionSyntaxError('unexpected end to tokens') 190 | 191 | index += 1 192 | 193 | -------------------------------------------------------------------------------- /src/pint/registry_helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.registry_helpers 4 | ~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Miscellaneous methods of the registry writen as separate functions. 7 | 8 | :copyright: 2016 by Pint Authors, see AUTHORS for more details.. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | import functools 13 | 14 | from .compat import string_types, zip_longest 15 | from .errors import DimensionalityError 16 | from .util import to_units_container, UnitsContainer 17 | 18 | try: 19 | from inspect import signature 20 | except ImportError: 21 | # Python2 does not have the inspect library. Import the backport. 22 | from funcsigs import signature 23 | 24 | 25 | def _replace_units(original_units, values_by_name): 26 | """Convert a unit compatible type to a UnitsContainer. 27 | 28 | :param original_units: a UnitsContainer instance. 29 | :param values_by_name: a map between original names and the new values. 30 | """ 31 | q = 1 32 | for arg_name, exponent in original_units.items(): 33 | q = q * values_by_name[arg_name] ** exponent 34 | 35 | return getattr(q, "_units", UnitsContainer({})) 36 | 37 | 38 | def _to_units_container(a, registry=None): 39 | """Convert a unit compatible type to a UnitsContainer, 40 | checking if it is string field prefixed with an equal 41 | (which is considered a reference) 42 | 43 | Return a tuple with the unit container and a boolean indicating if it was a reference. 44 | """ 45 | if isinstance(a, string_types) and '=' in a: 46 | return to_units_container(a.split('=', 1)[1]), True 47 | return to_units_container(a, registry), False 48 | 49 | 50 | def _parse_wrap_args(args, registry=None): 51 | 52 | # Arguments which contain definitions 53 | # (i.e. names that appear alone and for the first time) 54 | defs_args = set() 55 | defs_args_ndx = set() 56 | 57 | # Arguments which depend on others 58 | dependent_args_ndx = set() 59 | 60 | # Arguments which have units. 61 | unit_args_ndx = set() 62 | 63 | # _to_units_container 64 | args_as_uc = [_to_units_container(arg, registry) for arg in args] 65 | 66 | # Check for references in args, remove None values 67 | for ndx, (arg, is_ref) in enumerate(args_as_uc): 68 | if arg is None: 69 | continue 70 | elif is_ref: 71 | if len(arg) == 1: 72 | [(key, value)] = arg.items() 73 | if value == 1 and key not in defs_args: 74 | # This is the first time that 75 | # a variable is used => it is a definition. 76 | defs_args.add(key) 77 | defs_args_ndx.add(ndx) 78 | args_as_uc[ndx] = (key, True) 79 | else: 80 | # The variable was already found elsewhere, 81 | # we consider it a dependent variable. 82 | dependent_args_ndx.add(ndx) 83 | else: 84 | dependent_args_ndx.add(ndx) 85 | else: 86 | unit_args_ndx.add(ndx) 87 | 88 | # Check that all valid dependent variables 89 | for ndx in dependent_args_ndx: 90 | arg, is_ref = args_as_uc[ndx] 91 | if not isinstance(arg, dict): 92 | continue 93 | if not set(arg.keys()) <= defs_args: 94 | raise ValueError('Found a missing token while wrapping a function: ' 95 | 'Not all variable referenced in %s are defined using !' % args[ndx]) 96 | 97 | def _converter(ureg, values, strict): 98 | new_values = list(value for value in values) 99 | 100 | values_by_name = {} 101 | 102 | # first pass: Grab named values 103 | for ndx in defs_args_ndx: 104 | value = values[ndx] 105 | values_by_name[args_as_uc[ndx][0]] = value 106 | new_values[ndx] = getattr(value, "_magnitude", value) 107 | 108 | # second pass: calculate derived values based on named values 109 | for ndx in dependent_args_ndx: 110 | value = values[ndx] 111 | assert _replace_units(args_as_uc[ndx][0], values_by_name) is not None 112 | new_values[ndx] = ureg._convert(getattr(value, "_magnitude", value), 113 | getattr(value, "_units", UnitsContainer({})), 114 | _replace_units(args_as_uc[ndx][0], values_by_name)) 115 | 116 | # third pass: convert other arguments 117 | for ndx in unit_args_ndx: 118 | 119 | if isinstance(values[ndx], ureg.Quantity): 120 | new_values[ndx] = ureg._convert(values[ndx]._magnitude, 121 | values[ndx]._units, 122 | args_as_uc[ndx][0]) 123 | else: 124 | if strict: 125 | raise ValueError('A wrapped function using strict=True requires ' 126 | 'quantity for all arguments with not None units. ' 127 | '(error found for {}, {})'.format(args_as_uc[ndx][0], new_values[ndx])) 128 | 129 | return new_values, values_by_name 130 | 131 | return _converter 132 | 133 | def _apply_defaults(func, args, kwargs): 134 | """Apply default keyword arguments. 135 | 136 | Named keywords may have been left blank. This function applies the default 137 | values so that every argument is defined. 138 | """ 139 | 140 | sig = signature(func) 141 | bound_arguments = sig.bind(*args, **kwargs) 142 | for param in sig.parameters.values(): 143 | if param.name not in bound_arguments.arguments: 144 | bound_arguments.arguments[param.name] = param.default 145 | args = [bound_arguments.arguments[key] for key in sig.parameters.keys()] 146 | return args, {} 147 | 148 | def wraps(ureg, ret, args, strict=True): 149 | """Wraps a function to become pint-aware. 150 | 151 | Use it when a function requires a numerical value but in some specific 152 | units. The wrapper function will take a pint quantity, convert to the units 153 | specified in `args` and then call the wrapped function with the resulting 154 | magnitude. 155 | 156 | The value returned by the wrapped function will be converted to the units 157 | specified in `ret`. 158 | 159 | Use None to skip argument conversion. 160 | Set strict to False, to accept also numerical values. 161 | 162 | :param ureg: a UnitRegistry instance. 163 | :param ret: output units. 164 | :param args: iterable of input units. 165 | :param strict: boolean to indicate that only quantities are accepted. 166 | :return: the wrapped function. 167 | :raises: 168 | :class:`ValueError` if strict and one of the arguments is not a Quantity. 169 | """ 170 | 171 | if not isinstance(args, (list, tuple)): 172 | args = (args, ) 173 | 174 | converter = _parse_wrap_args(args) 175 | 176 | if isinstance(ret, (list, tuple)): 177 | container, ret = True, ret.__class__([_to_units_container(arg, ureg) for arg in ret]) 178 | else: 179 | container, ret = False, _to_units_container(ret, ureg) 180 | 181 | def decorator(func): 182 | assigned = tuple(attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr)) 183 | updated = tuple(attr for attr in functools.WRAPPER_UPDATES if hasattr(func, attr)) 184 | 185 | @functools.wraps(func, assigned=assigned, updated=updated) 186 | def wrapper(*values, **kw): 187 | 188 | values, kw = _apply_defaults(func, values, kw) 189 | 190 | # In principle, the values are used as is 191 | # When then extract the magnitudes when needed. 192 | new_values, values_by_name = converter(ureg, values, strict) 193 | 194 | result = func(*new_values, **kw) 195 | 196 | if container: 197 | out_units = (_replace_units(r, values_by_name) if is_ref else r 198 | for (r, is_ref) in ret) 199 | return ret.__class__(res if unit is None else ureg.Quantity(res, unit) 200 | for unit, res in zip_longest(out_units, result)) 201 | 202 | if ret[0] is None: 203 | return result 204 | 205 | return ureg.Quantity(result, 206 | _replace_units(ret[0], values_by_name) if ret[1] else ret[0]) 207 | 208 | return wrapper 209 | return decorator 210 | 211 | 212 | def check(ureg, *args): 213 | """Decorator to for quantity type checking for function inputs. 214 | 215 | Use it to ensure that the decorated function input parameters match 216 | the expected type of pint quantity. 217 | 218 | Use None to skip argument checking. 219 | 220 | :param ureg: a UnitRegistry instance. 221 | :param args: iterable of input units. 222 | :return: the wrapped function. 223 | :raises: 224 | :class:`DimensionalityError` if the parameters don't match dimensions 225 | """ 226 | dimensions = [ureg.get_dimensionality(dim) if dim is not None else None for dim in args] 227 | 228 | def decorator(func): 229 | assigned = tuple(attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr)) 230 | updated = tuple(attr for attr in functools.WRAPPER_UPDATES if hasattr(func, attr)) 231 | 232 | @functools.wraps(func, assigned=assigned, updated=updated) 233 | def wrapper(*args, **kwargs): 234 | list_args, empty = _apply_defaults(func, args, kwargs) 235 | if len(dimensions) > len(list_args): 236 | raise TypeError("%s takes %i parameters, but %i dimensions were passed" 237 | % (func.__name__, len(list_args), len(dimensions))) 238 | for dim, value in zip(dimensions, list_args): 239 | 240 | if dim is None: 241 | continue 242 | 243 | if not ureg.Quantity(value).check(dim): 244 | val_dim = ureg.get_dimensionality(value) 245 | raise DimensionalityError(value, 'a quantity of', 246 | val_dim, dim) 247 | return func(*args, **kwargs) 248 | return wrapper 249 | return decorator 250 | -------------------------------------------------------------------------------- /src/pint/testsuite/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import division, unicode_literals, print_function, absolute_import 4 | 5 | import doctest 6 | import logging 7 | import os 8 | import sys 9 | import unittest 10 | 11 | from contextlib import contextmanager 12 | 13 | from pint.compat import ndarray, np 14 | 15 | from pint import logger, UnitRegistry 16 | from pint.quantity import _Quantity 17 | from pint.testsuite.helpers import PintOutputChecker 18 | from logging.handlers import BufferingHandler 19 | 20 | 21 | class TestHandler(BufferingHandler): 22 | 23 | def __init__(self, only_warnings=False): 24 | # BufferingHandler takes a "capacity" argument 25 | # so as to know when to flush. As we're overriding 26 | # shouldFlush anyway, we can set a capacity of zero. 27 | # You can call flush() manually to clear out the 28 | # buffer. 29 | self.only_warnings = only_warnings 30 | BufferingHandler.__init__(self, 0) 31 | 32 | def shouldFlush(self): 33 | return False 34 | 35 | def emit(self, record): 36 | if self.only_warnings and record.level != logging.WARNING: 37 | return 38 | self.buffer.append(record.__dict__) 39 | 40 | 41 | class BaseTestCase(unittest.TestCase): 42 | 43 | CHECK_NO_WARNING = True 44 | 45 | @contextmanager 46 | def capture_log(self, level=logging.DEBUG): 47 | th = TestHandler() 48 | th.setLevel(level) 49 | logger.addHandler(th) 50 | if self._test_handler is not None: 51 | l = len(self._test_handler.buffer) 52 | yield th.buffer 53 | if self._test_handler is not None: 54 | self._test_handler.buffer = self._test_handler.buffer[:l] 55 | 56 | def setUp(self): 57 | self._test_handler = None 58 | if self.CHECK_NO_WARNING: 59 | self._test_handler = th = TestHandler() 60 | th.setLevel(logging.WARNING) 61 | logger.addHandler(th) 62 | 63 | def tearDown(self): 64 | if self._test_handler is not None: 65 | buf = self._test_handler.buffer 66 | l = len(buf) 67 | msg = '\n'.join(record.get('msg', str(record)) for record in buf) 68 | self.assertEqual(l, 0, msg='%d warnings raised.\n%s' % (l, msg)) 69 | 70 | 71 | class QuantityTestCase(BaseTestCase): 72 | 73 | FORCE_NDARRAY = False 74 | 75 | @classmethod 76 | def setUpClass(cls): 77 | cls.ureg = UnitRegistry(force_ndarray=cls.FORCE_NDARRAY) 78 | cls.Q_ = cls.ureg.Quantity 79 | cls.U_ = cls.ureg.Unit 80 | 81 | def _get_comparable_magnitudes(self, first, second, msg): 82 | if isinstance(first, _Quantity) and isinstance(second, _Quantity): 83 | second = second.to(first) 84 | self.assertEqual(first.units, second.units, msg=msg + ' Units are not equal.') 85 | m1, m2 = first.magnitude, second.magnitude 86 | elif isinstance(first, _Quantity): 87 | self.assertTrue(first.dimensionless, msg=msg + ' The first is not dimensionless.') 88 | first = first.to('') 89 | m1, m2 = first.magnitude, second 90 | elif isinstance(second, _Quantity): 91 | self.assertTrue(second.dimensionless, msg=msg + ' The second is not dimensionless.') 92 | second = second.to('') 93 | m1, m2 = first, second.magnitude 94 | else: 95 | m1, m2 = first, second 96 | 97 | return m1, m2 98 | 99 | def assertQuantityEqual(self, first, second, msg=None): 100 | if msg is None: 101 | msg = 'Comparing %r and %r. ' % (first, second) 102 | 103 | m1, m2 = self._get_comparable_magnitudes(first, second, msg) 104 | 105 | if isinstance(m1, ndarray) or isinstance(m2, ndarray): 106 | np.testing.assert_array_equal(m1, m2, err_msg=msg) 107 | else: 108 | self.assertEqual(m1, m2, msg) 109 | 110 | def assertQuantityAlmostEqual(self, first, second, rtol=1e-07, atol=0, msg=None): 111 | if msg is None: 112 | msg = 'Comparing %r and %r. ' % (first, second) 113 | 114 | m1, m2 = self._get_comparable_magnitudes(first, second, msg) 115 | 116 | if isinstance(m1, ndarray) or isinstance(m2, ndarray): 117 | np.testing.assert_allclose(m1, m2, rtol=rtol, atol=atol, err_msg=msg) 118 | else: 119 | self.assertLessEqual(abs(m1 - m2), atol + rtol * abs(m2), msg=msg) 120 | 121 | 122 | def testsuite(): 123 | """A testsuite that has all the pint tests. 124 | """ 125 | suite = unittest.TestLoader().discover(os.path.dirname(__file__)) 126 | from pint.compat import HAS_NUMPY, HAS_UNCERTAINTIES 127 | 128 | # TESTING THE DOCUMENTATION requires pyyaml, serialize, numpy and uncertainties 129 | if HAS_NUMPY and HAS_UNCERTAINTIES: 130 | try: 131 | import yaml, serialize 132 | add_docs(suite) 133 | except ImportError: 134 | pass 135 | return suite 136 | 137 | 138 | def main(): 139 | """Runs the testsuite as command line application. 140 | """ 141 | try: 142 | unittest.main() 143 | except Exception as e: 144 | print('Error: %s' % e) 145 | 146 | 147 | def run(): 148 | """Run all tests. 149 | 150 | :return: a :class:`unittest.TestResult` object 151 | """ 152 | test_runner = unittest.TextTestRunner() 153 | return test_runner.run(testsuite()) 154 | 155 | 156 | 157 | import math 158 | 159 | _GLOBS = { 160 | 'wrapping.rst': { 161 | 'pendulum_period': lambda length: 2*math.pi*math.sqrt(length/9.806650), 162 | 'pendulum_period2': lambda length, swing_amplitude: 1., 163 | 'pendulum_period_maxspeed': lambda length, swing_amplitude: (1., 2.), 164 | 'pendulum_period_error': lambda length: (1., False), 165 | } 166 | } 167 | 168 | 169 | def add_docs(suite): 170 | """Add docs to suite 171 | 172 | :type suite: unittest.TestSuite 173 | """ 174 | docpath = os.path.join(os.path.dirname(__file__), '..', '..', 'docs') 175 | docpath = os.path.abspath(docpath) 176 | if os.path.exists(docpath): 177 | checker = PintOutputChecker() 178 | for name in (name for name in os.listdir(docpath) if name.endswith('.rst')): 179 | file = os.path.join(docpath, name) 180 | suite.addTest(doctest.DocFileSuite(file, 181 | module_relative=False, 182 | checker=checker, 183 | globs=_GLOBS.get(name, None))) 184 | 185 | 186 | def test_docs(): 187 | suite = unittest.TestSuite() 188 | add_docs(suite) 189 | runner = unittest.TextTestRunner() 190 | return runner.run(suite) 191 | 192 | 193 | -------------------------------------------------------------------------------- /src/pint/testsuite/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import division, unicode_literals, print_function, absolute_import 4 | 5 | 6 | import doctest 7 | from distutils.version import StrictVersion 8 | import re 9 | import unittest 10 | 11 | from pint.compat import HAS_NUMPY, HAS_PROPER_BABEL, HAS_UNCERTAINTIES, NUMPY_VER, PYTHON3 12 | 13 | 14 | def requires_numpy18(): 15 | if not HAS_NUMPY: 16 | return unittest.skip('Requires NumPy') 17 | return unittest.skipUnless(StrictVersion(NUMPY_VER) >= StrictVersion('1.8'), 'Requires NumPy >= 1.8') 18 | 19 | 20 | def requires_numpy_previous_than(version): 21 | if not HAS_NUMPY: 22 | return unittest.skip('Requires NumPy') 23 | return unittest.skipUnless(StrictVersion(NUMPY_VER) < StrictVersion(version), 'Requires NumPy < %s' % version) 24 | 25 | 26 | def requires_numpy(): 27 | return unittest.skipUnless(HAS_NUMPY, 'Requires NumPy') 28 | 29 | 30 | def requires_not_numpy(): 31 | return unittest.skipIf(HAS_NUMPY, 'Requires NumPy is not installed.') 32 | 33 | 34 | def requires_proper_babel(): 35 | return unittest.skipUnless(HAS_PROPER_BABEL, 'Requires Babel with units support') 36 | 37 | 38 | def requires_uncertainties(): 39 | return unittest.skipUnless(HAS_UNCERTAINTIES, 'Requires Uncertainties') 40 | 41 | 42 | def requires_not_uncertainties(): 43 | return unittest.skipIf(HAS_UNCERTAINTIES, 'Requires Uncertainties is not installed.') 44 | 45 | 46 | def requires_python2(): 47 | return unittest.skipIf(PYTHON3, 'Requires Python 2.X.') 48 | 49 | 50 | def requires_python3(): 51 | return unittest.skipUnless(PYTHON3, 'Requires Python 3.X.') 52 | 53 | 54 | _number_re = '([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)' 55 | _q_re = re.compile('%s)' % _number_re + 56 | '\s*,\s*' + "'(?P.*)'" + '\s*' + '\)>') 57 | 58 | _sq_re = re.compile('\s*' + '(?P%s)' % _number_re + 59 | '\s' + "(?P.*)") 60 | 61 | _unit_re = re.compile('') 62 | 63 | 64 | class PintOutputChecker(doctest.OutputChecker): 65 | 66 | def check_output(self, want, got, optionflags): 67 | check = super(PintOutputChecker, self).check_output(want, got, optionflags) 68 | if check: 69 | return check 70 | 71 | try: 72 | if eval(want) == eval(got): 73 | return True 74 | except: 75 | pass 76 | 77 | for regex in (_q_re, _sq_re): 78 | try: 79 | parsed_got = regex.match(got.replace(r'\\', '')).groupdict() 80 | parsed_want = regex.match(want.replace(r'\\', '')).groupdict() 81 | 82 | v1 = float(parsed_got['magnitude']) 83 | v2 = float(parsed_want['magnitude']) 84 | 85 | if abs(v1 - v2) > abs(v1) / 1000: 86 | return False 87 | 88 | if parsed_got['unit'] != parsed_want['unit']: 89 | return False 90 | 91 | return True 92 | except: 93 | pass 94 | 95 | cnt = 0 96 | for regex in (_unit_re, ): 97 | try: 98 | parsed_got, tmp = regex.subn('\1', got) 99 | cnt += tmp 100 | parsed_want, temp = regex.subn('\1', want) 101 | cnt += tmp 102 | 103 | if parsed_got == parsed_want: 104 | return True 105 | 106 | except: 107 | pass 108 | 109 | if cnt: 110 | # If there was any replacement, we try again the previous methods. 111 | return self.check_output(parsed_want, parsed_got, optionflags) 112 | 113 | return False 114 | 115 | -------------------------------------------------------------------------------- /src/pint/testsuite/parameterized.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Adds Parameterized tests for Python's unittest module 4 | # 5 | # Code from: parameterizedtestcase, version: 0.1.0 6 | # Homepage: https://github.com/msabramo/python_unittest_parameterized_test_case 7 | # Author: Marc Abramowitz, email: marc@marc-abramowitz.com 8 | # License: MIT 9 | # 10 | # Fixed for to work in Python 2 & 3 with "add_metaclass" decorator from six 11 | # https://pypi.python.org/pypi/six 12 | # Author: Benjamin Peterson 13 | # License: MIT 14 | # 15 | # Use like this: 16 | # 17 | # from parameterizedtestcase import ParameterizedTestCase 18 | # 19 | # class MyTests(ParameterizedTestCase): 20 | # @ParameterizedTestCase.parameterize( 21 | # ("input", "expected_output"), 22 | # [ 23 | # ("2+4", 6), 24 | # ("3+5", 8), 25 | # ("6*9", 54), 26 | # ] 27 | # ) 28 | # def test_eval(self, input, expected_output): 29 | # self.assertEqual(eval(input), expected_output) 30 | 31 | from functools import wraps 32 | import collections 33 | import unittest 34 | 35 | def add_metaclass(metaclass): 36 | """Class decorator for creating a class with a metaclass.""" 37 | def wrapper(cls): 38 | orig_vars = cls.__dict__.copy() 39 | orig_vars.pop('__dict__', None) 40 | orig_vars.pop('__weakref__', None) 41 | slots = orig_vars.get('__slots__') 42 | if slots is not None: 43 | if isinstance(slots, str): 44 | slots = [slots] 45 | for slots_var in slots: 46 | orig_vars.pop(slots_var) 47 | return metaclass(cls.__name__, cls.__bases__, orig_vars) 48 | return wrapper 49 | 50 | 51 | def augment_method_docstring(method, new_class_dict, classname, 52 | param_names, param_values, new_method): 53 | param_assignments_str = '; '.join( 54 | ['%s = %s' % (k, v) for (k, v) in zip(param_names, param_values)]) 55 | extra_doc = "%s (%s.%s) [with %s] " % ( 56 | method.__name__, new_class_dict.get('__module__', ''), 57 | classname, param_assignments_str) 58 | 59 | try: 60 | new_method.__doc__ = extra_doc + new_method.__doc__ 61 | except TypeError: # Catches when new_method.__doc__ is None 62 | new_method.__doc__ = extra_doc 63 | 64 | 65 | class ParameterizedTestCaseMetaClass(type): 66 | method_counter = {} 67 | 68 | def __new__(meta, classname, bases, class_dict): 69 | new_class_dict = {} 70 | 71 | for attr_name, attr_value in list(class_dict.items()): 72 | if isinstance(attr_value, collections.Callable) and hasattr(attr_value, 'param_names'): 73 | # print("Processing attr_name = %r; attr_value = %r" % ( 74 | # attr_name, attr_value)) 75 | 76 | method = attr_value 77 | param_names = attr_value.param_names 78 | data = attr_value.data 79 | func_name_format = attr_value.func_name_format 80 | 81 | meta.process_method( 82 | classname, method, param_names, data, new_class_dict, 83 | func_name_format) 84 | else: 85 | new_class_dict[attr_name] = attr_value 86 | 87 | return type.__new__(meta, classname, bases, new_class_dict) 88 | 89 | @classmethod 90 | def process_method( 91 | cls, classname, method, param_names, data, new_class_dict, 92 | func_name_format): 93 | method_counter = cls.method_counter 94 | 95 | for param_values in data: 96 | new_method = cls.new_method(method, param_values) 97 | method_counter[method.__name__] = \ 98 | method_counter.get(method.__name__, 0) + 1 99 | case_data = dict(list(zip(param_names, param_values))) 100 | case_data['func_name'] = method.__name__ 101 | case_data['case_num'] = method_counter[method.__name__] 102 | 103 | new_method.__name__ = func_name_format.format(**case_data) 104 | 105 | augment_method_docstring( 106 | method, new_class_dict, classname, 107 | param_names, param_values, new_method) 108 | new_class_dict[new_method.__name__] = new_method 109 | 110 | @classmethod 111 | def new_method(cls, method, param_values): 112 | @wraps(method) 113 | def new_method(self): 114 | return method(self, *param_values) 115 | 116 | return new_method 117 | 118 | @add_metaclass(ParameterizedTestCaseMetaClass) 119 | class ParameterizedTestMixin(object): 120 | @classmethod 121 | def parameterize(cls, param_names, data, 122 | func_name_format='{func_name}_{case_num:05d}'): 123 | """Decorator for parameterizing a test method - example: 124 | 125 | @ParameterizedTestCase.parameterize( 126 | ("isbn", "expected_title"), [ 127 | ("0262033844", "Introduction to Algorithms"), 128 | ("0321558146", "Campbell Essential Biology")]) 129 | 130 | """ 131 | 132 | def decorator(func): 133 | @wraps(func) 134 | def newfunc(*arg, **kwargs): 135 | return func(*arg, **kwargs) 136 | 137 | newfunc.param_names = param_names 138 | newfunc.data = data 139 | newfunc.func_name_format = func_name_format 140 | 141 | return newfunc 142 | 143 | return decorator 144 | 145 | @add_metaclass(ParameterizedTestCaseMetaClass) 146 | class ParameterizedTestCase(unittest.TestCase, ParameterizedTestMixin): 147 | pass 148 | -------------------------------------------------------------------------------- /src/pint/testsuite/test_babel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import division, unicode_literals, print_function, absolute_import 3 | 4 | from pint.testsuite import helpers, BaseTestCase 5 | from pint import UnitRegistry 6 | import os 7 | 8 | class TestBabel(BaseTestCase): 9 | 10 | @helpers.requires_proper_babel() 11 | def test_babel(self): 12 | ureg = UnitRegistry() 13 | dirname = os.path.dirname(__file__) 14 | ureg.load_definitions(os.path.join(dirname, '../xtranslated.txt')) 15 | 16 | distance = 24.0 * ureg.meter 17 | self.assertEqual( 18 | distance.format_babel(locale='fr_FR', length='long'), 19 | "24.0 mètres" 20 | ) 21 | time = 8.0 * ureg.second 22 | self.assertEqual( 23 | time.format_babel(locale='fr_FR', length='long'), 24 | "8.0 secondes" 25 | ) 26 | self.assertEqual( 27 | time.format_babel(locale='ro', length='short'), 28 | "8.0 s" 29 | ) 30 | acceleration = distance / time ** 2 31 | self.assertEqual( 32 | acceleration.format_babel(locale='fr_FR', length='long'), 33 | "0.375 mètre par seconde²" 34 | ) 35 | mks = ureg.get_system('mks') 36 | self.assertEqual( 37 | mks.format_babel(locale='fr_FR'), 38 | "métrique" 39 | ) 40 | -------------------------------------------------------------------------------- /src/pint/testsuite/test_converters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import division, unicode_literals, print_function, absolute_import 3 | 4 | import itertools 5 | 6 | from pint.compat import np 7 | from pint.converters import (ScaleConverter, OffsetConverter, Converter) 8 | from pint.testsuite import helpers, BaseTestCase 9 | 10 | class TestConverter(BaseTestCase): 11 | 12 | def test_converter(self): 13 | c = Converter() 14 | self.assertTrue(c.is_multiplicative) 15 | self.assertTrue(c.to_reference(8)) 16 | self.assertTrue(c.from_reference(8)) 17 | 18 | def test_multiplicative_converter(self): 19 | c = ScaleConverter(20.) 20 | self.assertEqual(c.from_reference(c.to_reference(100)), 100) 21 | self.assertEqual(c.to_reference(c.from_reference(100)), 100) 22 | 23 | def test_offset_converter(self): 24 | c = OffsetConverter(20., 2) 25 | self.assertEqual(c.from_reference(c.to_reference(100)), 100) 26 | self.assertEqual(c.to_reference(c.from_reference(100)), 100) 27 | 28 | @helpers.requires_numpy() 29 | def test_converter_inplace(self): 30 | for c in (ScaleConverter(20.), OffsetConverter(20., 2)): 31 | fun1 = lambda x, y: c.from_reference(c.to_reference(x, y), y) 32 | fun2 = lambda x, y: c.to_reference(c.from_reference(x, y), y) 33 | for fun, (inplace, comp) in itertools.product((fun1, fun2), 34 | ((True, self.assertIs), (False, self.assertIsNot))): 35 | a = np.ones((1, 10)) 36 | ac = np.ones((1, 10)) 37 | r = fun(a, inplace) 38 | np.testing.assert_allclose(r, ac) 39 | comp(a, r) 40 | -------------------------------------------------------------------------------- /src/pint/testsuite/test_definitions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import division, unicode_literals, print_function, absolute_import 4 | 5 | from pint.util import (UnitsContainer) 6 | from pint.converters import (ScaleConverter, OffsetConverter) 7 | from pint.definitions import (Definition, PrefixDefinition, UnitDefinition, 8 | DimensionDefinition) 9 | 10 | from pint.testsuite import BaseTestCase 11 | 12 | class TestDefinition(BaseTestCase): 13 | 14 | def test_invalid(self): 15 | self.assertRaises(ValueError, Definition.from_string, 'x = [time] * meter') 16 | self.assertRaises(ValueError, Definition.from_string, '[x] = [time] * meter') 17 | 18 | def test_prefix_definition(self): 19 | for definition in ('m- = 1e-3', 'm- = 10**-3', 'm- = 0.001'): 20 | x = Definition.from_string(definition) 21 | self.assertIsInstance(x, PrefixDefinition) 22 | self.assertEqual(x.name, 'm') 23 | self.assertEqual(x.aliases, ()) 24 | self.assertEqual(x.converter.to_reference(1000), 1) 25 | self.assertEqual(x.converter.from_reference(0.001), 1) 26 | self.assertEqual(str(x), 'm') 27 | 28 | x = Definition.from_string('kilo- = 1e-3 = k-') 29 | self.assertIsInstance(x, PrefixDefinition) 30 | self.assertEqual(x.name, 'kilo') 31 | self.assertEqual(x.aliases, ()) 32 | self.assertEqual(x.symbol, 'k') 33 | self.assertEqual(x.converter.to_reference(1000), 1) 34 | self.assertEqual(x.converter.from_reference(.001), 1) 35 | 36 | x = Definition.from_string('kilo- = 1e-3 = k- = anotherk-') 37 | self.assertIsInstance(x, PrefixDefinition) 38 | self.assertEqual(x.name, 'kilo') 39 | self.assertEqual(x.aliases, ('anotherk', )) 40 | self.assertEqual(x.symbol, 'k') 41 | self.assertEqual(x.converter.to_reference(1000), 1) 42 | self.assertEqual(x.converter.from_reference(.001), 1) 43 | 44 | def test_baseunit_definition(self): 45 | x = Definition.from_string('meter = [length]') 46 | self.assertIsInstance(x, UnitDefinition) 47 | self.assertTrue(x.is_base) 48 | self.assertEqual(x.reference, UnitsContainer({'[length]': 1})) 49 | 50 | def test_unit_definition(self): 51 | x = Definition.from_string('coulomb = ampere * second') 52 | self.assertIsInstance(x, UnitDefinition) 53 | self.assertFalse(x.is_base) 54 | self.assertIsInstance(x.converter, ScaleConverter) 55 | self.assertEqual(x.converter.scale, 1) 56 | self.assertEqual(x.reference, UnitsContainer(ampere=1, second=1)) 57 | 58 | x = Definition.from_string('faraday = 96485.3399 * coulomb') 59 | self.assertIsInstance(x, UnitDefinition) 60 | self.assertFalse(x.is_base) 61 | self.assertIsInstance(x.converter, ScaleConverter) 62 | self.assertEqual(x.converter.scale, 96485.3399) 63 | self.assertEqual(x.reference, UnitsContainer(coulomb=1)) 64 | 65 | x = Definition.from_string('degF = 9 / 5 * kelvin; offset: 255.372222') 66 | self.assertIsInstance(x, UnitDefinition) 67 | self.assertFalse(x.is_base) 68 | self.assertIsInstance(x.converter, OffsetConverter) 69 | self.assertEqual(x.converter.scale, 9/5) 70 | self.assertEqual(x.converter.offset, 255.372222) 71 | self.assertEqual(x.reference, UnitsContainer(kelvin=1)) 72 | 73 | def test_dimension_definition(self): 74 | x = DimensionDefinition('[time]', '', (), converter='') 75 | self.assertTrue(x.is_base) 76 | self.assertEqual(x.name, '[time]') 77 | 78 | x = Definition.from_string('[speed] = [length]/[time]') 79 | self.assertIsInstance(x, DimensionDefinition) 80 | self.assertEqual(x.reference, UnitsContainer({'[length]': 1, '[time]': -1})) 81 | -------------------------------------------------------------------------------- /src/pint/testsuite/test_errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import division, unicode_literals, print_function, absolute_import 3 | 4 | 5 | from pint.errors import DimensionalityError, UndefinedUnitError 6 | from pint.testsuite import BaseTestCase 7 | 8 | class TestErrors(BaseTestCase): 9 | 10 | def test_errors(self): 11 | x = ('meter', ) 12 | msg = "'meter' is not defined in the unit registry" 13 | self.assertEqual(str(UndefinedUnitError(x)), msg) 14 | self.assertEqual(str(UndefinedUnitError(list(x))), msg) 15 | self.assertEqual(str(UndefinedUnitError(set(x))), msg) 16 | 17 | msg = "Cannot convert from 'a' (c) to 'b' (d)msg" 18 | ex = DimensionalityError('a', 'b', 'c', 'd', 'msg') 19 | self.assertEqual(str(ex), msg) 20 | -------------------------------------------------------------------------------- /src/pint/testsuite/test_formatter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import division, unicode_literals, print_function, absolute_import 4 | 5 | from pint import formatting as fmt 6 | from pint.testsuite import QuantityTestCase 7 | 8 | 9 | class TestFormatter(QuantityTestCase): 10 | 11 | def test_join(self): 12 | for empty in (tuple(), []): 13 | self.assertEqual(fmt._join('s', empty), '') 14 | self.assertEqual(fmt._join('*', '1 2 3'.split()), '1*2*3') 15 | self.assertEqual(fmt._join('{0}*{1}', '1 2 3'.split()), '1*2*3') 16 | 17 | 18 | def test_formatter(self): 19 | self.assertEqual(fmt.formatter(dict().items()), '') 20 | self.assertEqual(fmt.formatter(dict(meter=1).items()), 'meter') 21 | self.assertEqual(fmt.formatter(dict(meter=-1).items()), '1 / meter') 22 | self.assertEqual(fmt.formatter(dict(meter=-1).items(), as_ratio=False), 'meter ** -1') 23 | 24 | self.assertEqual(fmt.formatter(dict(meter=-1, second=-1).items(), as_ratio=False), 25 | 'meter ** -1 * second ** -1') 26 | self.assertEqual(fmt.formatter(dict(meter=-1, second=-1).items()), 27 | '1 / meter / second') 28 | self.assertEqual(fmt.formatter(dict(meter=-1, second=-1).items(), single_denominator=True), 29 | '1 / (meter * second)') 30 | self.assertEqual(fmt.formatter(dict(meter=-1, second=-2).items()), 31 | '1 / meter / second ** 2') 32 | self.assertEqual(fmt.formatter(dict(meter=-1, second=-2).items(), single_denominator=True), 33 | '1 / (meter * second ** 2)') 34 | 35 | def test_parse_spec(self): 36 | self.assertEqual(fmt._parse_spec(''), '') 37 | self.assertEqual(fmt._parse_spec(''), '') 38 | self.assertRaises(ValueError, fmt._parse_spec, 'W') 39 | self.assertRaises(ValueError, fmt._parse_spec, 'PL') 40 | 41 | def test_format_unit(self): 42 | self.assertEqual(fmt.format_unit('', 'C'), 'dimensionless') 43 | self.assertRaises(ValueError, fmt.format_unit, 'm', 'W') 44 | -------------------------------------------------------------------------------- /src/pint/testsuite/test_infer_base_unit.py: -------------------------------------------------------------------------------- 1 | from pint import UnitRegistry, set_application_registry 2 | from pint.testsuite import QuantityTestCase 3 | from pint.util import infer_base_unit 4 | 5 | ureg = UnitRegistry() 6 | set_application_registry(ureg) 7 | Q = ureg.Quantity 8 | 9 | 10 | class TestInferBaseUnit(QuantityTestCase): 11 | def test_infer_base_unit(self): 12 | from pint.util import infer_base_unit 13 | self.assertEqual(infer_base_unit(Q(1, 'millimeter * nanometer')), Q(1, 'meter**2').units) 14 | 15 | def test_units_adding_to_zero(self): 16 | self.assertEqual(infer_base_unit(Q(1, 'm * mm / m / um * s')), Q(1, 's').units) 17 | 18 | def test_to_compact(self): 19 | r = Q(1000000000, 'm') * Q(1, 'mm') / Q(1, 's') / Q(1, 'ms') 20 | compact_r = r.to_compact() 21 | expected = Q(1000., 'kilometer**2 / second**2') 22 | self.assertQuantityAlmostEqual(compact_r, expected) 23 | 24 | r = (Q(1, 'm') * Q(1, 'mm') / Q(1, 'm') / Q(2, 'um') * Q(2, 's')).to_compact() 25 | self.assertQuantityAlmostEqual(r, Q(1000, 's')) 26 | 27 | def test_volts(self): 28 | from pint.util import infer_base_unit 29 | r = Q(1, 'V') * Q(1, 'mV') / Q(1, 'kV') 30 | b = infer_base_unit(r) 31 | self.assertEqual(b, Q(1, 'V').units) 32 | self.assertQuantityAlmostEqual(r, Q(1, 'uV')) -------------------------------------------------------------------------------- /src/pint/testsuite/test_measurement.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import division, unicode_literals, print_function, absolute_import 4 | 5 | from pint.testsuite import QuantityTestCase, helpers 6 | 7 | 8 | @helpers.requires_not_uncertainties() 9 | class TestNotMeasurement(QuantityTestCase): 10 | 11 | FORCE_NDARRAY = False 12 | 13 | def test_instantiate(self): 14 | M_ = self.ureg.Measurement 15 | self.assertRaises(RuntimeError, M_, 4.0, 0.1, 's') 16 | 17 | 18 | @helpers.requires_uncertainties() 19 | class TestMeasurement(QuantityTestCase): 20 | 21 | FORCE_NDARRAY = False 22 | 23 | def test_simple(self): 24 | M_ = self.ureg.Measurement 25 | M_(4.0, 0.1, 's') 26 | 27 | def test_build(self): 28 | M_ = self.ureg.Measurement 29 | v, u = self.Q_(4.0, 's'), self.Q_(.1, 's') 30 | M_(v.magnitude, u.magnitude, 's') 31 | ms = (M_(v.magnitude, u.magnitude, 's'), 32 | M_(v, u.magnitude), 33 | M_(v, u), 34 | v.plus_minus(.1), 35 | v.plus_minus(0.025, True), 36 | v.plus_minus(u),) 37 | 38 | for m in ms: 39 | self.assertEqual(m.value, v) 40 | self.assertEqual(m.error, u) 41 | self.assertEqual(m.rel, m.error / abs(m.value)) 42 | 43 | def test_format(self): 44 | v, u = self.Q_(4.0, 's ** 2'), self.Q_(.1, 's ** 2') 45 | m = self.ureg.Measurement(v, u) 46 | self.assertEqual(str(m), '(4.00 +/- 0.10) second ** 2') 47 | self.assertEqual(repr(m), '') 48 | #self.assertEqual('{:!s}'.format(m), '(4.00 +/- 0.10) second ** 2') 49 | #self.assertEqual('{:!r}'.format(m), '') 50 | self.assertEqual('{0:P}'.format(m), '(4.00 ± 0.10) second²') 51 | self.assertEqual('{0:L}'.format(m), r'\left(4.00 \pm 0.10\right)\ \mathrm{second}^{2}') 52 | self.assertEqual('{0:H}'.format(m), '(4.00 ± 0.10) second2') 53 | self.assertEqual('{0:C}'.format(m), '(4.00+/-0.10) second**2') 54 | self.assertEqual('{0:Lx}'.format(m), r'\SI[separate-uncertainty=true]{4.00(10)}{\second\squared}') 55 | self.assertEqual('{0:.1f}'.format(m), '(4.0 +/- 0.1) second ** 2') 56 | self.assertEqual('{0:.1fP}'.format(m), '(4.0 ± 0.1) second²') 57 | self.assertEqual('{0:.1fL}'.format(m), r'\left(4.0 \pm 0.1\right)\ \mathrm{second}^{2}') 58 | self.assertEqual('{0:.1fH}'.format(m), '(4.0 ± 0.1) second2') 59 | self.assertEqual('{0:.1fC}'.format(m), '(4.0+/-0.1) second**2') 60 | self.assertEqual('{0:.1fLx}'.format(m), '\SI[separate-uncertainty=true]{4.0(1)}{\second\squared}') 61 | 62 | def test_format_paru(self): 63 | v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') 64 | m = self.ureg.Measurement(v, u) 65 | self.assertEqual('{0:uS}'.format(m), '0.200(10) second ** 2') 66 | self.assertEqual('{0:.3uS}'.format(m), '0.2000(100) second ** 2') 67 | self.assertEqual('{0:.3uSP}'.format(m), '0.2000(100) second²') 68 | self.assertEqual('{0:.3uSL}'.format(m), r'0.2000\left(100\right)\ \mathrm{second}^{2}') 69 | self.assertEqual('{0:.3uSH}'.format(m), '0.2000(100) second2') 70 | self.assertEqual('{0:.3uSC}'.format(m), '0.2000(100) second**2') 71 | 72 | def test_format_u(self): 73 | v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') 74 | m = self.ureg.Measurement(v, u) 75 | self.assertEqual('{0:.3u}'.format(m), '(0.2000 +/- 0.0100) second ** 2') 76 | self.assertEqual('{0:.3uP}'.format(m), '(0.2000 ± 0.0100) second²') 77 | self.assertEqual('{0:.3uL}'.format(m), r'\left(0.2000 \pm 0.0100\right)\ \mathrm{second}^{2}') 78 | self.assertEqual('{0:.3uH}'.format(m), '(0.2000 ± 0.0100) second2') 79 | self.assertEqual('{0:.3uC}'.format(m), '(0.2000+/-0.0100) second**2') 80 | self.assertEqual('{0:.3uLx}'.format(m), '\SI[separate-uncertainty=true]{0.2000(100)}{\second\squared}') 81 | self.assertEqual('{0:.1uLx}'.format(m), '\SI[separate-uncertainty=true]{0.20(1)}{\second\squared}') 82 | 83 | def test_format_percu(self): 84 | self.test_format_perce() 85 | v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') 86 | m = self.ureg.Measurement(v, u) 87 | self.assertEqual('{0:.1u%}'.format(m), '(20 +/- 1)% second ** 2') 88 | self.assertEqual('{0:.1u%P}'.format(m), '(20 ± 1)% second²') 89 | self.assertEqual('{0:.1u%L}'.format(m), r'\left(20 \pm 1\right) \%\ \mathrm{second}^{2}') 90 | self.assertEqual('{0:.1u%H}'.format(m), '(20 ± 1)% second2') 91 | self.assertEqual('{0:.1u%C}'.format(m), '(20+/-1)% second**2') 92 | 93 | def test_format_perce(self): 94 | v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') 95 | m = self.ureg.Measurement(v, u) 96 | self.assertEqual('{0:.1ue}'.format(m), '(2.0 +/- 0.1)e-01 second ** 2') 97 | self.assertEqual('{0:.1ueP}'.format(m), '(2.0 ± 0.1)×10⁻¹ second²') 98 | self.assertEqual('{0:.1ueL}'.format(m), r'\left(2.0 \pm 0.1\right) \times 10^{-1}\ \mathrm{second}^{2}') 99 | self.assertEqual('{0:.1ueH}'.format(m), '(2.0 ± 0.1)e-01 second2') 100 | self.assertEqual('{0:.1ueC}'.format(m), '(2.0+/-0.1)e-01 second**2') 101 | 102 | def test_raise_build(self): 103 | v, u = self.Q_(1.0, 's'), self.Q_(.1, 's') 104 | o = self.Q_(.1, 'm') 105 | 106 | M_ = self.ureg.Measurement 107 | self.assertRaises(ValueError, M_, v, o) 108 | self.assertRaises(ValueError, v.plus_minus, o) 109 | self.assertRaises(ValueError, v.plus_minus, u, True) 110 | 111 | def test_propagate_linear(self): 112 | 113 | v1, u1 = self.Q_(8.0, 's'), self.Q_(.7, 's') 114 | v2, u2 = self.Q_(5.0, 's'), self.Q_(.6, 's') 115 | v2, u3 = self.Q_(-5.0, 's'), self.Q_(.6, 's') 116 | 117 | m1 = v1.plus_minus(u1) 118 | m2 = v2.plus_minus(u2) 119 | m3 = v2.plus_minus(u3) 120 | 121 | for factor, m in zip((3, -3, 3, -3), (m1, m3, m1, m3)): 122 | r = factor * m 123 | self.assertAlmostEqual(r.value.magnitude, factor * m.value.magnitude) 124 | self.assertAlmostEqual(r.error.magnitude, abs(factor * m.error.magnitude)) 125 | self.assertEqual(r.value.units, m.value.units) 126 | 127 | for ml, mr in zip((m1, m1, m1, m3), (m1, m2, m3, m3)): 128 | r = ml + mr 129 | self.assertAlmostEqual(r.value.magnitude, ml.value.magnitude + mr.value.magnitude) 130 | self.assertAlmostEqual(r.error.magnitude, 131 | ml.error.magnitude + mr.error.magnitude if ml is mr else 132 | (ml.error.magnitude ** 2 + mr.error.magnitude ** 2) ** .5) 133 | self.assertEqual(r.value.units, ml.value.units) 134 | 135 | for ml, mr in zip((m1, m1, m1, m3), (m1, m2, m3, m3)): 136 | r = ml - mr 137 | self.assertAlmostEqual(r.value.magnitude, ml.value.magnitude - mr.value.magnitude) 138 | self.assertAlmostEqual(r.error.magnitude, 139 | 0 if ml is mr else 140 | (ml.error.magnitude ** 2 + mr.error.magnitude ** 2) ** .5) 141 | self.assertEqual(r.value.units, ml.value.units) 142 | 143 | def test_propagate_product(self): 144 | 145 | v1, u1 = self.Q_(8.0, 's'), self.Q_(.7, 's') 146 | v2, u2 = self.Q_(5.0, 's'), self.Q_(.6, 's') 147 | v2, u3 = self.Q_(-5.0, 's'), self.Q_(.6, 's') 148 | 149 | m1 = v1.plus_minus(u1) 150 | m2 = v2.plus_minus(u2) 151 | m3 = v2.plus_minus(u3) 152 | 153 | m4 = (2.3 * self.ureg.meter).plus_minus(0.1) 154 | m5 = (1.4 * self.ureg.meter).plus_minus(0.2) 155 | 156 | for ml, mr in zip((m1, m1, m1, m3, m4), (m1, m2, m3, m3, m5)): 157 | r = ml * mr 158 | self.assertAlmostEqual(r.value.magnitude, ml.value.magnitude * mr.value.magnitude) 159 | self.assertEqual(r.value.units, ml.value.units * mr.value.units) 160 | 161 | for ml, mr in zip((m1, m1, m1, m3, m4), (m1, m2, m3, m3, m5)): 162 | r = ml / mr 163 | self.assertAlmostEqual(r.value.magnitude, ml.value.magnitude / mr.value.magnitude) 164 | self.assertEqual(r.value.units, ml.value.units / mr.value.units) 165 | -------------------------------------------------------------------------------- /src/pint/testsuite/test_pint_eval.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import division, unicode_literals, print_function, absolute_import 4 | import unittest 5 | 6 | from pint.compat import tokenizer 7 | from pint.pint_eval import build_eval_tree 8 | 9 | 10 | class TestPintEval(unittest.TestCase): 11 | 12 | def _test_one(self, input_text, parsed): 13 | self.assertEqual(build_eval_tree(tokenizer(input_text)).to_string(), parsed) 14 | 15 | def test_build_eval_tree(self): 16 | self._test_one('3', '3') 17 | self._test_one('1 + 2', '(1 + 2)') 18 | # order of operations 19 | self._test_one('2 * 3 + 4', '((2 * 3) + 4)') 20 | # parentheses 21 | self._test_one('2 * (3 + 4)', '(2 * (3 + 4))') 22 | # more order of operations 23 | self._test_one('1 + 2 * 3 ** (4 + 3 / 5)', '(1 + (2 * (3 ** (4 + (3 / 5)))))') 24 | # nested parentheses at beginning 25 | self._test_one('1 * ((3 + 4) * 5)', '(1 * ((3 + 4) * 5))') 26 | # nested parentheses at end 27 | self._test_one('1 * (5 * (3 + 4))', '(1 * (5 * (3 + 4)))') 28 | # nested parentheses in middle 29 | self._test_one('1 * (5 * (3 + 4) / 6)', '(1 * ((5 * (3 + 4)) / 6))') 30 | # unary 31 | self._test_one('-1', '(- 1)') 32 | # unary 33 | self._test_one('3 * -1', '(3 * (- 1))') 34 | # double unary 35 | self._test_one('3 * --1', '(3 * (- (- 1)))') 36 | # parenthetical unary 37 | self._test_one('3 * -(2 + 4)', '(3 * (- (2 + 4)))') 38 | # parenthetical unary 39 | self._test_one('3 * -((2 + 4))', '(3 * (- (2 + 4)))') 40 | # implicit op 41 | self._test_one('3 4', '(3 4)') 42 | # implicit op, then parentheses 43 | self._test_one('3 (2 + 4)', '(3 (2 + 4))') 44 | # parentheses, then implicit 45 | self._test_one('(3 ** 4 ) 5', '((3 ** 4) 5)') 46 | # implicit op, then exponentiation 47 | self._test_one('3 4 ** 5', '(3 (4 ** 5))') 48 | # implicit op, then addition 49 | self._test_one('3 4 + 5', '((3 4) + 5)') 50 | # power followed by implicit 51 | self._test_one('3 ** 4 5', '((3 ** 4) 5)') 52 | # implicit with parentheses 53 | self._test_one('3 (4 ** 5)', '(3 (4 ** 5))') 54 | # exponent with e 55 | self._test_one('3e-1', '3e-1') 56 | # multiple units with exponents 57 | self._test_one('kg ** 1 * s ** 2', '((kg ** 1) * (s ** 2))') 58 | # multiple units with neg exponents 59 | self._test_one('kg ** -1 * s ** -2', '((kg ** (- 1)) * (s ** (- 2)))') 60 | # multiple units with neg exponents 61 | self._test_one('kg^-1 * s^-2', '((kg ^ (- 1)) * (s ^ (- 2)))') 62 | # multiple units with neg exponents, implicit op 63 | self._test_one('kg^-1 s^-2', '((kg ^ (- 1)) (s ^ (- 2)))') 64 | # nested power 65 | self._test_one('2 ^ 3 ^ 2', '(2 ^ (3 ^ 2))') 66 | # nested power 67 | self._test_one('gram * second / meter ** 2', '((gram * second) / (meter ** 2))') 68 | # nested power 69 | self._test_one('gram / meter ** 2 / second', '((gram / (meter ** 2)) / second)') 70 | # units should behave like numbers, so we don't need a bunch of extra tests for them 71 | # implicit op, then addition 72 | self._test_one('3 kg + 5', '((3 kg) + 5)') 73 | -------------------------------------------------------------------------------- /src/pint/testsuite/test_pitheorem.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import division, unicode_literals, print_function, absolute_import 4 | 5 | import itertools 6 | 7 | from pint import pi_theorem 8 | 9 | from pint.testsuite import QuantityTestCase 10 | 11 | 12 | class TestPiTheorem(QuantityTestCase): 13 | 14 | FORCE_NDARRAY = False 15 | 16 | def test_simple(self): 17 | 18 | # simple movement 19 | with self.capture_log() as buffer: 20 | self.assertEqual(pi_theorem({'V': 'm/s', 'T': 's', 'L': 'm'}), 21 | [{'V': 1, 'T': 1, 'L': -1}]) 22 | 23 | # pendulum 24 | self.assertEqual(pi_theorem({'T': 's', 'M': 'grams', 'L': 'm', 'g': 'm/s**2'}), 25 | [{'g': 1, 'T': 2, 'L': -1}]) 26 | self.assertEqual(len(buffer), 7) 27 | 28 | def test_inputs(self): 29 | V = 'km/hour' 30 | T = 'ms' 31 | L = 'cm' 32 | 33 | f1 = lambda x: x 34 | f2 = lambda x: self.Q_(1, x) 35 | f3 = lambda x: self.Q_(1, x).units 36 | f4 = lambda x: self.Q_(1, x).dimensionality 37 | 38 | fs = f1, f2, f3, f4 39 | for fv, ft, fl in itertools.product(fs, fs, fs): 40 | qv = fv(V) 41 | qt = ft(T) 42 | ql = ft(L) 43 | self.assertEqual(self.ureg.pi_theorem({'V': qv, 'T': qt, 'L': ql}), 44 | [{'V': 1.0, 'T': 1.0, 'L': -1.0}]) 45 | -------------------------------------------------------------------------------- /src/pint/unit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pint.unit 4 | ~~~~~~~~~ 5 | 6 | Functions and classes related to unit definitions and conversions. 7 | 8 | :copyright: 2016 by Pint Authors, see AUTHORS for more details. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | from __future__ import division, unicode_literals, print_function, absolute_import 13 | 14 | import copy 15 | import operator 16 | from numbers import Number 17 | 18 | from .util import ( 19 | PrettyIPython, UnitsContainer, SharedRegistryObject, fix_str_conversions) 20 | 21 | from .compat import string_types, NUMERIC_TYPES, long_type 22 | from .formatting import siunitx_format_unit 23 | from .definitions import UnitDefinition 24 | 25 | 26 | @fix_str_conversions 27 | class _Unit(PrettyIPython, SharedRegistryObject): 28 | """Implements a class to describe a unit supporting math operations. 29 | 30 | :type units: UnitsContainer, str, Unit or Quantity. 31 | 32 | """ 33 | 34 | #: Default formatting string. 35 | default_format = '' 36 | 37 | def __reduce__(self): 38 | from . import _build_unit 39 | return _build_unit, (self._units, ) 40 | 41 | def __new__(cls, units): 42 | inst = object.__new__(cls) 43 | if isinstance(units, (UnitsContainer, UnitDefinition)): 44 | inst._units = units 45 | elif isinstance(units, string_types): 46 | inst._units = inst._REGISTRY.parse_units(units)._units 47 | elif isinstance(units, _Unit): 48 | inst._units = units._units 49 | else: 50 | raise TypeError('units must be of type str, Unit or ' 51 | 'UnitsContainer; not {}.'.format(type(units))) 52 | 53 | inst.__used = False 54 | inst.__handling = None 55 | return inst 56 | 57 | @property 58 | def debug_used(self): 59 | return self.__used 60 | 61 | def __copy__(self): 62 | ret = self.__class__(self._units) 63 | ret.__used = self.__used 64 | return ret 65 | 66 | def __deepcopy__(self, memo): 67 | ret = self.__class__(copy.deepcopy(self._units)) 68 | ret.__used = self.__used 69 | return ret 70 | 71 | def __str__(self): 72 | return format(self) 73 | 74 | def __repr__(self): 75 | return "".format(self._units) 76 | 77 | def __format__(self, spec): 78 | spec = spec or self.default_format 79 | # special cases 80 | if 'Lx' in spec: # the LaTeX siunitx code 81 | opts = '' 82 | ustr = siunitx_format_unit(self) 83 | ret = r'\si[%s]{%s}'%( opts, ustr ) 84 | return ret 85 | 86 | 87 | if '~' in spec: 88 | if not self._units: 89 | return '' 90 | units = UnitsContainer(dict((self._REGISTRY._get_symbol(key), 91 | value) 92 | for key, value in self._units.items())) 93 | spec = spec.replace('~', '') 94 | else: 95 | units = self._units 96 | 97 | return '%s' % (format(units, spec)) 98 | 99 | def format_babel(self, spec='', **kwspec): 100 | spec = spec or self.default_format 101 | 102 | if '~' in spec: 103 | if self.dimensionless: 104 | return '' 105 | units = UnitsContainer(dict((self._REGISTRY._get_symbol(key), 106 | value) 107 | for key, value in self._units.items())) 108 | spec = spec.replace('~', '') 109 | else: 110 | units = self._units 111 | 112 | return '%s' % (units.format_babel(spec, **kwspec)) 113 | 114 | @property 115 | def dimensionless(self): 116 | """Return true if the Unit is dimensionless. 117 | 118 | """ 119 | return not bool(self.dimensionality) 120 | 121 | @property 122 | def dimensionality(self): 123 | """Unit's dimensionality (e.g. {length: 1, time: -1}) 124 | 125 | """ 126 | try: 127 | return self._dimensionality 128 | except AttributeError: 129 | dim = self._REGISTRY._get_dimensionality(self._units) 130 | self._dimensionality = dim 131 | 132 | return self._dimensionality 133 | 134 | def compatible_units(self, *contexts): 135 | if contexts: 136 | with self._REGISTRY.context(*contexts): 137 | return self._REGISTRY.get_compatible_units(self) 138 | 139 | return self._REGISTRY.get_compatible_units(self) 140 | 141 | def __mul__(self, other): 142 | if self._check(other): 143 | if isinstance(other, self.__class__): 144 | return self.__class__(self._units*other._units) 145 | else: 146 | qself = self._REGISTRY.Quantity(1.0, self._units) 147 | return qself * other 148 | 149 | if isinstance(other, Number) and other == 1: 150 | return self._REGISTRY.Quantity(other, self._units) 151 | 152 | return self._REGISTRY.Quantity(1, self._units) * other 153 | 154 | __rmul__ = __mul__ 155 | 156 | def __truediv__(self, other): 157 | if self._check(other): 158 | if isinstance(other, self.__class__): 159 | return self.__class__(self._units/other._units) 160 | else: 161 | qself = 1.0 * self 162 | return qself / other 163 | 164 | return self._REGISTRY.Quantity(1/other, self._units) 165 | 166 | def __rtruediv__(self, other): 167 | # As Unit and Quantity both handle truediv with each other rtruediv can 168 | # only be called for something different. 169 | if isinstance(other, NUMERIC_TYPES): 170 | return self._REGISTRY.Quantity(other, 1/self._units) 171 | elif isinstance(other, UnitsContainer): 172 | return self.__class__(other/self._units) 173 | else: 174 | return NotImplemented 175 | 176 | __div__ = __truediv__ 177 | __rdiv__ = __rtruediv__ 178 | 179 | def __pow__(self, other): 180 | if isinstance(other, NUMERIC_TYPES): 181 | return self.__class__(self._units**other) 182 | 183 | else: 184 | mess = 'Cannot power Unit by {}'.format(type(other)) 185 | raise TypeError(mess) 186 | 187 | def __hash__(self): 188 | return self._units.__hash__() 189 | 190 | def __eq__(self, other): 191 | # We compare to the base class of Unit because each Unit class is 192 | # unique. 193 | if self._check(other): 194 | if isinstance(other, self.__class__): 195 | return self._units == other._units 196 | else: 197 | return other == self._REGISTRY.Quantity(1, self._units) 198 | 199 | elif isinstance(other, NUMERIC_TYPES): 200 | return other == self._REGISTRY.Quantity(1, self._units) 201 | 202 | else: 203 | return self._units == other 204 | 205 | def __ne__(self, other): 206 | return not (self == other) 207 | 208 | def compare(self, other, op): 209 | self_q = self._REGISTRY.Quantity(1, self) 210 | 211 | if isinstance(other, NUMERIC_TYPES): 212 | return self_q.compare(other, op) 213 | elif isinstance(other, (_Unit, UnitsContainer, dict)): 214 | return self_q.compare(self._REGISTRY.Quantity(1, other), op) 215 | else: 216 | return NotImplemented 217 | 218 | __lt__ = lambda self, other: self.compare(other, op=operator.lt) 219 | __le__ = lambda self, other: self.compare(other, op=operator.le) 220 | __ge__ = lambda self, other: self.compare(other, op=operator.ge) 221 | __gt__ = lambda self, other: self.compare(other, op=operator.gt) 222 | 223 | def __int__(self): 224 | return int(self._REGISTRY.Quantity(1, self._units)) 225 | 226 | def __long__(self): 227 | return long_type(self._REGISTRY.Quantity(1, self._units)) 228 | 229 | def __float__(self): 230 | return float(self._REGISTRY.Quantity(1, self._units)) 231 | 232 | def __complex__(self): 233 | return complex(self._REGISTRY.Quantity(1, self._units)) 234 | 235 | __array_priority__ = 17 236 | 237 | def __array_prepare__(self, array, context=None): 238 | return 1 239 | 240 | def __array_wrap__(self, array, context=None): 241 | uf, objs, huh = context 242 | 243 | if uf.__name__ in ('true_divide', 'divide', 'floor_divide'): 244 | return self._REGISTRY.Quantity(array, 1/self._units) 245 | elif uf.__name__ in ('multiply',): 246 | return self._REGISTRY.Quantity(array, self._units) 247 | else: 248 | raise ValueError('Unsupproted operation for Unit') 249 | 250 | @property 251 | def systems(self): 252 | out = set() 253 | for uname in self._units.keys(): 254 | for sname, sys in self._REGISTRY._systems.items(): 255 | if uname in sys.members: 256 | out.add(sname) 257 | return frozenset(out) 258 | 259 | def from_(self, value, strict=True, name='value'): 260 | """Converts a numerical value or quantity to this unit 261 | 262 | :param value: a Quantity (or numerical value if strict=False) to convert 263 | :param strict: boolean to indicate that only quanities are accepted 264 | :param name: descriptive name to use if an exception occurs 265 | :return: The converted value as this unit 266 | :raises: 267 | :class:`ValueError` if strict and one of the arguments is not a Quantity. 268 | """ 269 | if self._check(value): 270 | if not isinstance(value, self._REGISTRY.Quantity): 271 | value = self._REGISTRY.Quantity(1, value) 272 | return value.to(self) 273 | elif strict: 274 | raise ValueError("%s must be a Quantity" % value) 275 | else: 276 | return value * self 277 | 278 | def m_from(self, value, strict=True, name='value'): 279 | """Converts a numerical value or quantity to this unit, then returns 280 | the magnitude of the converted value 281 | 282 | :param value: a Quantity (or numerical value if strict=False) to convert 283 | :param strict: boolean to indicate that only quanities are accepted 284 | :param name: descriptive name to use if an exception occurs 285 | :return: The magnitude of the converted value 286 | :raises: 287 | :class:`ValueError` if strict and one of the arguments is not a Quantity. 288 | """ 289 | return self.from_(value, strict=strict, name=name).magnitude 290 | 291 | def build_unit_class(registry): 292 | 293 | class Unit(_Unit): 294 | pass 295 | 296 | Unit._REGISTRY = registry 297 | return Unit 298 | -------------------------------------------------------------------------------- /src/pint/xtranslated.txt: -------------------------------------------------------------------------------- 1 | 2 | # a few unit definitions added to use the translations by unicode cldr 3 | 4 | dietary_calorie = 1000 * calorie = Calorie 5 | metric_cup = liter / 4 6 | mps = meter / second 7 | square_inch = inch ** 2 = sq_in 8 | square_mile = mile ** 2 = sq_mile 9 | square_meter = kilometer ** 2 = sq_m 10 | square_kilometer = kilometer ** 2 = sq_km 11 | mile_scandinavian = 10000 * meter 12 | century = 100 * year 13 | cubic_mile = 1 * mile ** 3 = cu_mile = cubic_miles 14 | cubic_yard = 1 * yard ** 3 = cu_yd = cubic_yards 15 | cubic_foot = 1 * foot ** 3 = cu_ft = cubic_feet 16 | cubic_inch = 1 * inch ** 3 = cu_in = cubic_inches 17 | cubic_meter = 1 * meter ** 3 = cu_m 18 | cubic_kilometer = 1 * kilometer ** 3 = cu_km 19 | karat = [purity] = Karat 20 | 21 | [consumption] = [volume] / [length] 22 | liter_per_kilometer = liter / kilometer 23 | liter_per_100kilometers = liter / (100 * kilometers) 24 | 25 | [US_consumption] = [length] / [volume] 26 | MPG = mile / gallon 27 | -------------------------------------------------------------------------------- /src/pkg_resources/_vendor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-convert/97407f4ec8dbca5abbc6952b2b56cf3918624177/src/pkg_resources/_vendor/__init__.py -------------------------------------------------------------------------------- /src/pkg_resources/_vendor/packaging/__about__.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | from __future__ import absolute_import, division, print_function 5 | 6 | __all__ = [ 7 | "__title__", "__summary__", "__uri__", "__version__", "__author__", 8 | "__email__", "__license__", "__copyright__", 9 | ] 10 | 11 | __title__ = "packaging" 12 | __summary__ = "Core utilities for Python packages" 13 | __uri__ = "https://github.com/pypa/packaging" 14 | 15 | __version__ = "16.8" 16 | 17 | __author__ = "Donald Stufft and individual contributors" 18 | __email__ = "donald@stufft.io" 19 | 20 | __license__ = "BSD or Apache License, Version 2.0" 21 | __copyright__ = "Copyright 2014-2016 %s" % __author__ 22 | -------------------------------------------------------------------------------- /src/pkg_resources/_vendor/packaging/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | from __future__ import absolute_import, division, print_function 5 | 6 | from .__about__ import ( 7 | __author__, __copyright__, __email__, __license__, __summary__, __title__, 8 | __uri__, __version__ 9 | ) 10 | 11 | __all__ = [ 12 | "__title__", "__summary__", "__uri__", "__version__", "__author__", 13 | "__email__", "__license__", "__copyright__", 14 | ] 15 | -------------------------------------------------------------------------------- /src/pkg_resources/_vendor/packaging/_compat.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | from __future__ import absolute_import, division, print_function 5 | 6 | import sys 7 | 8 | 9 | PY2 = sys.version_info[0] == 2 10 | PY3 = sys.version_info[0] == 3 11 | 12 | # flake8: noqa 13 | 14 | if PY3: 15 | string_types = str, 16 | else: 17 | string_types = basestring, 18 | 19 | 20 | def with_metaclass(meta, *bases): 21 | """ 22 | Create a base class with a metaclass. 23 | """ 24 | # This requires a bit of explanation: the basic idea is to make a dummy 25 | # metaclass for one level of class instantiation that replaces itself with 26 | # the actual metaclass. 27 | class metaclass(meta): 28 | def __new__(cls, name, this_bases, d): 29 | return meta(name, bases, d) 30 | return type.__new__(metaclass, 'temporary_class', (), {}) 31 | -------------------------------------------------------------------------------- /src/pkg_resources/_vendor/packaging/_structures.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | from __future__ import absolute_import, division, print_function 5 | 6 | 7 | class Infinity(object): 8 | 9 | def __repr__(self): 10 | return "Infinity" 11 | 12 | def __hash__(self): 13 | return hash(repr(self)) 14 | 15 | def __lt__(self, other): 16 | return False 17 | 18 | def __le__(self, other): 19 | return False 20 | 21 | def __eq__(self, other): 22 | return isinstance(other, self.__class__) 23 | 24 | def __ne__(self, other): 25 | return not isinstance(other, self.__class__) 26 | 27 | def __gt__(self, other): 28 | return True 29 | 30 | def __ge__(self, other): 31 | return True 32 | 33 | def __neg__(self): 34 | return NegativeInfinity 35 | 36 | Infinity = Infinity() 37 | 38 | 39 | class NegativeInfinity(object): 40 | 41 | def __repr__(self): 42 | return "-Infinity" 43 | 44 | def __hash__(self): 45 | return hash(repr(self)) 46 | 47 | def __lt__(self, other): 48 | return True 49 | 50 | def __le__(self, other): 51 | return True 52 | 53 | def __eq__(self, other): 54 | return isinstance(other, self.__class__) 55 | 56 | def __ne__(self, other): 57 | return not isinstance(other, self.__class__) 58 | 59 | def __gt__(self, other): 60 | return False 61 | 62 | def __ge__(self, other): 63 | return False 64 | 65 | def __neg__(self): 66 | return Infinity 67 | 68 | NegativeInfinity = NegativeInfinity() 69 | -------------------------------------------------------------------------------- /src/pkg_resources/_vendor/packaging/markers.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | from __future__ import absolute_import, division, print_function 5 | 6 | import operator 7 | import os 8 | import platform 9 | import sys 10 | 11 | from pkg_resources.extern.pyparsing import ParseException, ParseResults, stringStart, stringEnd 12 | from pkg_resources.extern.pyparsing import ZeroOrMore, Group, Forward, QuotedString 13 | from pkg_resources.extern.pyparsing import Literal as L # noqa 14 | 15 | from ._compat import string_types 16 | from .specifiers import Specifier, InvalidSpecifier 17 | 18 | 19 | __all__ = [ 20 | "InvalidMarker", "UndefinedComparison", "UndefinedEnvironmentName", 21 | "Marker", "default_environment", 22 | ] 23 | 24 | 25 | class InvalidMarker(ValueError): 26 | """ 27 | An invalid marker was found, users should refer to PEP 508. 28 | """ 29 | 30 | 31 | class UndefinedComparison(ValueError): 32 | """ 33 | An invalid operation was attempted on a value that doesn't support it. 34 | """ 35 | 36 | 37 | class UndefinedEnvironmentName(ValueError): 38 | """ 39 | A name was attempted to be used that does not exist inside of the 40 | environment. 41 | """ 42 | 43 | 44 | class Node(object): 45 | 46 | def __init__(self, value): 47 | self.value = value 48 | 49 | def __str__(self): 50 | return str(self.value) 51 | 52 | def __repr__(self): 53 | return "<{0}({1!r})>".format(self.__class__.__name__, str(self)) 54 | 55 | def serialize(self): 56 | raise NotImplementedError 57 | 58 | 59 | class Variable(Node): 60 | 61 | def serialize(self): 62 | return str(self) 63 | 64 | 65 | class Value(Node): 66 | 67 | def serialize(self): 68 | return '"{0}"'.format(self) 69 | 70 | 71 | class Op(Node): 72 | 73 | def serialize(self): 74 | return str(self) 75 | 76 | 77 | VARIABLE = ( 78 | L("implementation_version") | 79 | L("platform_python_implementation") | 80 | L("implementation_name") | 81 | L("python_full_version") | 82 | L("platform_release") | 83 | L("platform_version") | 84 | L("platform_machine") | 85 | L("platform_system") | 86 | L("python_version") | 87 | L("sys_platform") | 88 | L("os_name") | 89 | L("os.name") | # PEP-345 90 | L("sys.platform") | # PEP-345 91 | L("platform.version") | # PEP-345 92 | L("platform.machine") | # PEP-345 93 | L("platform.python_implementation") | # PEP-345 94 | L("python_implementation") | # undocumented setuptools legacy 95 | L("extra") 96 | ) 97 | ALIASES = { 98 | 'os.name': 'os_name', 99 | 'sys.platform': 'sys_platform', 100 | 'platform.version': 'platform_version', 101 | 'platform.machine': 'platform_machine', 102 | 'platform.python_implementation': 'platform_python_implementation', 103 | 'python_implementation': 'platform_python_implementation' 104 | } 105 | VARIABLE.setParseAction(lambda s, l, t: Variable(ALIASES.get(t[0], t[0]))) 106 | 107 | VERSION_CMP = ( 108 | L("===") | 109 | L("==") | 110 | L(">=") | 111 | L("<=") | 112 | L("!=") | 113 | L("~=") | 114 | L(">") | 115 | L("<") 116 | ) 117 | 118 | MARKER_OP = VERSION_CMP | L("not in") | L("in") 119 | MARKER_OP.setParseAction(lambda s, l, t: Op(t[0])) 120 | 121 | MARKER_VALUE = QuotedString("'") | QuotedString('"') 122 | MARKER_VALUE.setParseAction(lambda s, l, t: Value(t[0])) 123 | 124 | BOOLOP = L("and") | L("or") 125 | 126 | MARKER_VAR = VARIABLE | MARKER_VALUE 127 | 128 | MARKER_ITEM = Group(MARKER_VAR + MARKER_OP + MARKER_VAR) 129 | MARKER_ITEM.setParseAction(lambda s, l, t: tuple(t[0])) 130 | 131 | LPAREN = L("(").suppress() 132 | RPAREN = L(")").suppress() 133 | 134 | MARKER_EXPR = Forward() 135 | MARKER_ATOM = MARKER_ITEM | Group(LPAREN + MARKER_EXPR + RPAREN) 136 | MARKER_EXPR << MARKER_ATOM + ZeroOrMore(BOOLOP + MARKER_EXPR) 137 | 138 | MARKER = stringStart + MARKER_EXPR + stringEnd 139 | 140 | 141 | def _coerce_parse_result(results): 142 | if isinstance(results, ParseResults): 143 | return [_coerce_parse_result(i) for i in results] 144 | else: 145 | return results 146 | 147 | 148 | def _format_marker(marker, first=True): 149 | assert isinstance(marker, (list, tuple, string_types)) 150 | 151 | # Sometimes we have a structure like [[...]] which is a single item list 152 | # where the single item is itself it's own list. In that case we want skip 153 | # the rest of this function so that we don't get extraneous () on the 154 | # outside. 155 | if (isinstance(marker, list) and len(marker) == 1 and 156 | isinstance(marker[0], (list, tuple))): 157 | return _format_marker(marker[0]) 158 | 159 | if isinstance(marker, list): 160 | inner = (_format_marker(m, first=False) for m in marker) 161 | if first: 162 | return " ".join(inner) 163 | else: 164 | return "(" + " ".join(inner) + ")" 165 | elif isinstance(marker, tuple): 166 | return " ".join([m.serialize() for m in marker]) 167 | else: 168 | return marker 169 | 170 | 171 | _operators = { 172 | "in": lambda lhs, rhs: lhs in rhs, 173 | "not in": lambda lhs, rhs: lhs not in rhs, 174 | "<": operator.lt, 175 | "<=": operator.le, 176 | "==": operator.eq, 177 | "!=": operator.ne, 178 | ">=": operator.ge, 179 | ">": operator.gt, 180 | } 181 | 182 | 183 | def _eval_op(lhs, op, rhs): 184 | try: 185 | spec = Specifier("".join([op.serialize(), rhs])) 186 | except InvalidSpecifier: 187 | pass 188 | else: 189 | return spec.contains(lhs) 190 | 191 | oper = _operators.get(op.serialize()) 192 | if oper is None: 193 | raise UndefinedComparison( 194 | "Undefined {0!r} on {1!r} and {2!r}.".format(op, lhs, rhs) 195 | ) 196 | 197 | return oper(lhs, rhs) 198 | 199 | 200 | _undefined = object() 201 | 202 | 203 | def _get_env(environment, name): 204 | value = environment.get(name, _undefined) 205 | 206 | if value is _undefined: 207 | raise UndefinedEnvironmentName( 208 | "{0!r} does not exist in evaluation environment.".format(name) 209 | ) 210 | 211 | return value 212 | 213 | 214 | def _evaluate_markers(markers, environment): 215 | groups = [[]] 216 | 217 | for marker in markers: 218 | assert isinstance(marker, (list, tuple, string_types)) 219 | 220 | if isinstance(marker, list): 221 | groups[-1].append(_evaluate_markers(marker, environment)) 222 | elif isinstance(marker, tuple): 223 | lhs, op, rhs = marker 224 | 225 | if isinstance(lhs, Variable): 226 | lhs_value = _get_env(environment, lhs.value) 227 | rhs_value = rhs.value 228 | else: 229 | lhs_value = lhs.value 230 | rhs_value = _get_env(environment, rhs.value) 231 | 232 | groups[-1].append(_eval_op(lhs_value, op, rhs_value)) 233 | else: 234 | assert marker in ["and", "or"] 235 | if marker == "or": 236 | groups.append([]) 237 | 238 | return any(all(item) for item in groups) 239 | 240 | 241 | def format_full_version(info): 242 | version = '{0.major}.{0.minor}.{0.micro}'.format(info) 243 | kind = info.releaselevel 244 | if kind != 'final': 245 | version += kind[0] + str(info.serial) 246 | return version 247 | 248 | 249 | def default_environment(): 250 | if hasattr(sys, 'implementation'): 251 | iver = format_full_version(sys.implementation.version) 252 | implementation_name = sys.implementation.name 253 | else: 254 | iver = '0' 255 | implementation_name = '' 256 | 257 | return { 258 | "implementation_name": implementation_name, 259 | "implementation_version": iver, 260 | "os_name": os.name, 261 | "platform_machine": platform.machine(), 262 | "platform_release": platform.release(), 263 | "platform_system": platform.system(), 264 | "platform_version": platform.version(), 265 | "python_full_version": platform.python_version(), 266 | "platform_python_implementation": platform.python_implementation(), 267 | "python_version": platform.python_version()[:3], 268 | "sys_platform": sys.platform, 269 | } 270 | 271 | 272 | class Marker(object): 273 | 274 | def __init__(self, marker): 275 | try: 276 | self._markers = _coerce_parse_result(MARKER.parseString(marker)) 277 | except ParseException as e: 278 | err_str = "Invalid marker: {0!r}, parse error at {1!r}".format( 279 | marker, marker[e.loc:e.loc + 8]) 280 | raise InvalidMarker(err_str) 281 | 282 | def __str__(self): 283 | return _format_marker(self._markers) 284 | 285 | def __repr__(self): 286 | return "".format(str(self)) 287 | 288 | def evaluate(self, environment=None): 289 | """Evaluate a marker. 290 | 291 | Return the boolean from evaluating the given marker against the 292 | environment. environment is an optional argument to override all or 293 | part of the determined environment. 294 | 295 | The environment is determined from the current Python process. 296 | """ 297 | current_environment = default_environment() 298 | if environment is not None: 299 | current_environment.update(environment) 300 | 301 | return _evaluate_markers(self._markers, current_environment) 302 | -------------------------------------------------------------------------------- /src/pkg_resources/_vendor/packaging/requirements.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | from __future__ import absolute_import, division, print_function 5 | 6 | import string 7 | import re 8 | 9 | from pkg_resources.extern.pyparsing import stringStart, stringEnd, originalTextFor, ParseException 10 | from pkg_resources.extern.pyparsing import ZeroOrMore, Word, Optional, Regex, Combine 11 | from pkg_resources.extern.pyparsing import Literal as L # noqa 12 | from pkg_resources.extern.six.moves.urllib import parse as urlparse 13 | 14 | from .markers import MARKER_EXPR, Marker 15 | from .specifiers import LegacySpecifier, Specifier, SpecifierSet 16 | 17 | 18 | class InvalidRequirement(ValueError): 19 | """ 20 | An invalid requirement was found, users should refer to PEP 508. 21 | """ 22 | 23 | 24 | ALPHANUM = Word(string.ascii_letters + string.digits) 25 | 26 | LBRACKET = L("[").suppress() 27 | RBRACKET = L("]").suppress() 28 | LPAREN = L("(").suppress() 29 | RPAREN = L(")").suppress() 30 | COMMA = L(",").suppress() 31 | SEMICOLON = L(";").suppress() 32 | AT = L("@").suppress() 33 | 34 | PUNCTUATION = Word("-_.") 35 | IDENTIFIER_END = ALPHANUM | (ZeroOrMore(PUNCTUATION) + ALPHANUM) 36 | IDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END)) 37 | 38 | NAME = IDENTIFIER("name") 39 | EXTRA = IDENTIFIER 40 | 41 | URI = Regex(r'[^ ]+')("url") 42 | URL = (AT + URI) 43 | 44 | EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA) 45 | EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras") 46 | 47 | VERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE) 48 | VERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE) 49 | 50 | VERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY 51 | VERSION_MANY = Combine(VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), 52 | joinString=",", adjacent=False)("_raw_spec") 53 | _VERSION_SPEC = Optional(((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY)) 54 | _VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or '') 55 | 56 | VERSION_SPEC = originalTextFor(_VERSION_SPEC)("specifier") 57 | VERSION_SPEC.setParseAction(lambda s, l, t: t[1]) 58 | 59 | MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker") 60 | MARKER_EXPR.setParseAction( 61 | lambda s, l, t: Marker(s[t._original_start:t._original_end]) 62 | ) 63 | MARKER_SEPERATOR = SEMICOLON 64 | MARKER = MARKER_SEPERATOR + MARKER_EXPR 65 | 66 | VERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER) 67 | URL_AND_MARKER = URL + Optional(MARKER) 68 | 69 | NAMED_REQUIREMENT = \ 70 | NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER) 71 | 72 | REQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd 73 | 74 | 75 | class Requirement(object): 76 | """Parse a requirement. 77 | 78 | Parse a given requirement string into its parts, such as name, specifier, 79 | URL, and extras. Raises InvalidRequirement on a badly-formed requirement 80 | string. 81 | """ 82 | 83 | # TODO: Can we test whether something is contained within a requirement? 84 | # If so how do we do that? Do we need to test against the _name_ of 85 | # the thing as well as the version? What about the markers? 86 | # TODO: Can we normalize the name and extra name? 87 | 88 | def __init__(self, requirement_string): 89 | try: 90 | req = REQUIREMENT.parseString(requirement_string) 91 | except ParseException as e: 92 | raise InvalidRequirement( 93 | "Invalid requirement, parse error at \"{0!r}\"".format( 94 | requirement_string[e.loc:e.loc + 8])) 95 | 96 | self.name = req.name 97 | if req.url: 98 | parsed_url = urlparse.urlparse(req.url) 99 | if not (parsed_url.scheme and parsed_url.netloc) or ( 100 | not parsed_url.scheme and not parsed_url.netloc): 101 | raise InvalidRequirement("Invalid URL given") 102 | self.url = req.url 103 | else: 104 | self.url = None 105 | self.extras = set(req.extras.asList() if req.extras else []) 106 | self.specifier = SpecifierSet(req.specifier) 107 | self.marker = req.marker if req.marker else None 108 | 109 | def __str__(self): 110 | parts = [self.name] 111 | 112 | if self.extras: 113 | parts.append("[{0}]".format(",".join(sorted(self.extras)))) 114 | 115 | if self.specifier: 116 | parts.append(str(self.specifier)) 117 | 118 | if self.url: 119 | parts.append("@ {0}".format(self.url)) 120 | 121 | if self.marker: 122 | parts.append("; {0}".format(self.marker)) 123 | 124 | return "".join(parts) 125 | 126 | def __repr__(self): 127 | return "".format(str(self)) 128 | -------------------------------------------------------------------------------- /src/pkg_resources/_vendor/packaging/utils.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | from __future__ import absolute_import, division, print_function 5 | 6 | import re 7 | 8 | 9 | _canonicalize_regex = re.compile(r"[-_.]+") 10 | 11 | 12 | def canonicalize_name(name): 13 | # This is taken from PEP 503. 14 | return _canonicalize_regex.sub("-", name).lower() 15 | -------------------------------------------------------------------------------- /src/pkg_resources/extern/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | class VendorImporter: 5 | """ 6 | A PEP 302 meta path importer for finding optionally-vendored 7 | or otherwise naturally-installed packages from root_name. 8 | """ 9 | 10 | def __init__(self, root_name, vendored_names=(), vendor_pkg=None): 11 | self.root_name = root_name 12 | self.vendored_names = set(vendored_names) 13 | self.vendor_pkg = vendor_pkg or root_name.replace('extern', '_vendor') 14 | 15 | @property 16 | def search_path(self): 17 | """ 18 | Search first the vendor package then as a natural package. 19 | """ 20 | yield self.vendor_pkg + '.' 21 | yield '' 22 | 23 | def find_module(self, fullname, path=None): 24 | """ 25 | Return self when fullname starts with root_name and the 26 | target module is one vendored through this importer. 27 | """ 28 | root, base, target = fullname.partition(self.root_name + '.') 29 | if root: 30 | return 31 | if not any(map(target.startswith, self.vendored_names)): 32 | return 33 | return self 34 | 35 | def load_module(self, fullname): 36 | """ 37 | Iterate over the search path to locate and load fullname. 38 | """ 39 | root, base, target = fullname.partition(self.root_name + '.') 40 | for prefix in self.search_path: 41 | try: 42 | extant = prefix + target 43 | __import__(extant) 44 | mod = sys.modules[extant] 45 | sys.modules[fullname] = mod 46 | # mysterious hack: 47 | # Remove the reference to the extant package/module 48 | # on later Python versions to cause relative imports 49 | # in the vendor package to resolve the same modules 50 | # as those going through this importer. 51 | if prefix and sys.version_info > (3, 3): 52 | del sys.modules[extant] 53 | return mod 54 | except ImportError: 55 | pass 56 | else: 57 | raise ImportError( 58 | "The '{target}' package is required; " 59 | "normally this is bundled with this package so if you get " 60 | "this warning, consult the packager of your " 61 | "distribution.".format(**locals()) 62 | ) 63 | 64 | def install(self): 65 | """ 66 | Install this importer into sys.meta_path if not already present. 67 | """ 68 | if self not in sys.meta_path: 69 | sys.meta_path.append(self) 70 | 71 | 72 | names = 'packaging', 'pyparsing', 'six', 'appdirs' 73 | VendorImporter(__name__, names).install() 74 | -------------------------------------------------------------------------------- /src/pkg_resources/py31compat.py: -------------------------------------------------------------------------------- 1 | import os 2 | import errno 3 | import sys 4 | 5 | from .extern import six 6 | 7 | 8 | def _makedirs_31(path, exist_ok=False): 9 | try: 10 | os.makedirs(path) 11 | except OSError as exc: 12 | if not exist_ok or exc.errno != errno.EEXIST: 13 | raise 14 | 15 | 16 | # rely on compatibility behavior until mode considerations 17 | # and exists_ok considerations are disentangled. 18 | # See https://github.com/pypa/setuptools/pull/1083#issuecomment-315168663 19 | needs_makedirs = ( 20 | six.PY2 or 21 | (3, 4) <= sys.version_info < (3, 4, 1) 22 | ) 23 | makedirs = _makedirs_31 if needs_makedirs else os.makedirs 24 | -------------------------------------------------------------------------------- /src/test_convert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2017 Dean Jackson 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2017-07-16 9 | # 10 | 11 | """Test converter.""" 12 | 13 | from __future__ import print_function, absolute_import 14 | 15 | from collections import namedtuple 16 | import logging 17 | 18 | import pytest 19 | from workflow import Workflow3 20 | 21 | import convert 22 | from defaults import Defaults 23 | 24 | logging.basicConfig(level=logging.DEBUG) 25 | log = logging.getLogger() 26 | convert.log = logging.getLogger('convert') 27 | 28 | 29 | T = namedtuple('T', 'number dimensionality from_unit to_unit') 30 | C = namedtuple('C', 'from_number from_unit to_number to_unit dimensionality') 31 | 32 | 33 | def verify_parsed(t1, t2): 34 | """Verify results of `Converter.parse()`.""" 35 | assert t1.number == t2.number 36 | assert t1.dimensionality == t2.dimensionality 37 | assert t1.from_unit == t2.from_unit 38 | assert t1.to_unit == t2.to_unit 39 | 40 | 41 | def verify_conversion(c1, c2): 42 | """Verify results of `Converter.convert()`.""" 43 | assert c1.from_number == c2.from_number 44 | assert c1.from_unit == c2.from_unit 45 | assert c1.to_number == c2.to_number 46 | assert c1.to_unit == c2.to_unit 47 | assert c1.dimensionality == c2.dimensionality 48 | 49 | 50 | def test_invalid(): 51 | """Test invalid input.""" 52 | queries = [ 53 | 'dave', # doesn't start with a number 54 | '1.3', # no unit 55 | '5 daves', # invalid units 56 | '10 km m cm', # too many units 57 | ] 58 | c = convert.Converter(None) 59 | for query in queries: 60 | with pytest.raises(ValueError): 61 | c.parse(query) 62 | 63 | 64 | def test_valid(): 65 | """Test valid input.""" 66 | data = [ 67 | ('1.3 km', T(1.3, '[length]', 'kilometer', None)), 68 | ('1.3 km miles', T(1.3, '[length]', 'kilometer', 'mile')), 69 | ('5 m/s kph', T(5.0, '[length] / [time]', 'meter/second', 'kph')), 70 | ('21.3 m^2 acres', T(21.3, '[length] ** 2', u'meter²', 'acre')), 71 | ] 72 | c = convert.Converter(None) 73 | for t in data: 74 | i = c.parse(t[0]) 75 | verify_parsed(t[1], i) 76 | 77 | 78 | def test_conversion(): 79 | """Test conversions.""" 80 | data = [ 81 | ('1km m', C(1, 'kilometer', 1000, 'meter', '[length]')), 82 | ] 83 | c = convert.Converter(None) 84 | for t in data: 85 | i = c.parse(t[0]) 86 | res = c.convert(i) 87 | verify_conversion(t[1], res[0]) 88 | 89 | 90 | def test_defaults(): 91 | """Test default conversions.""" 92 | data = [ 93 | ('1m', [ 94 | C(1, 'meter', 100, 'centimeter', '[length]'), 95 | C(1, 'meter', 0.001, 'kilometer', '[length]')]), 96 | ('100g', [ 97 | C(100, 'gram', 0.1, 'kilogram', '[mass]'), 98 | C(100, 'gram', 100000, 'milligram', '[mass]')]), 99 | ] 100 | 101 | wf = Workflow3() 102 | if 'default_units' not in wf.settings: 103 | wf.settings['default_units'] = {} 104 | 105 | wf.settings['default_units']['[length]'] = ['centimeter', 'kilometer'] 106 | wf.settings['default_units']['[mass]'] = ['kilogram', 'milligram'] 107 | 108 | c = convert.Converter(Defaults(wf)) 109 | 110 | for t in data: 111 | i = c.parse(t[0]) 112 | res = c.convert(i) 113 | assert len(res) == len(t[1]) 114 | for j, r in enumerate(res): 115 | log.debug(r) 116 | verify_conversion(t[1][j], r) 117 | 118 | 119 | 120 | if __name__ == '__main__': # pragma: no cover 121 | pytest.main([__file__]) 122 | -------------------------------------------------------------------------------- /src/unit_definitions.txt: -------------------------------------------------------------------------------- 1 | # This file contains additional units for the Pint library. 2 | # These units are registered automatically when the workflow 3 | # is run. 4 | # 5 | # If you want to add your own units, *don't* add them here! 6 | # Add them to the `unit_definitions.txt` file in the workflow's 7 | # data directory. You can edit this file by entering "convinfo" 8 | # in Alfred and selecting "Edit Custom Units". 9 | 10 | # Barrel of oil equivalent 11 | barrel_of_oil_equivalent = 169.902 m**3 = boe 12 | 13 | # Million standard cubic feed 14 | MMscf = 28.31685 l * 1000000 = mmscf 15 | -------------------------------------------------------------------------------- /src/unit_definitions.txt.sample: -------------------------------------------------------------------------------- 1 | # Add your custom definitions to this file. See the Pint documentation for 2 | # information on the necessary format: 3 | # http://pint.readthedocs.org/en/latest/defining.html 4 | # You can see the default definitions built in to Pint here: 5 | # https://github.com/hgrecco/pint/blob/master/pint/default_en.txt 6 | 7 | -------------------------------------------------------------------------------- /src/workflow/Notify.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-convert/97407f4ec8dbca5abbc6952b2b56cf3918624177/src/workflow/Notify.tgz -------------------------------------------------------------------------------- /src/workflow/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2014 Dean Jackson 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2014-02-15 9 | # 10 | 11 | """A helper library for `Alfred `_ workflows.""" 12 | 13 | import os 14 | 15 | # Workflow objects 16 | from .workflow import Workflow, manager 17 | from .workflow3 import Variables, Workflow3 18 | 19 | # Exceptions 20 | from .workflow import PasswordNotFound, KeychainError 21 | 22 | # Icons 23 | from .workflow import ( 24 | ICON_ACCOUNT, 25 | ICON_BURN, 26 | ICON_CLOCK, 27 | ICON_COLOR, 28 | ICON_COLOUR, 29 | ICON_EJECT, 30 | ICON_ERROR, 31 | ICON_FAVORITE, 32 | ICON_FAVOURITE, 33 | ICON_GROUP, 34 | ICON_HELP, 35 | ICON_HOME, 36 | ICON_INFO, 37 | ICON_NETWORK, 38 | ICON_NOTE, 39 | ICON_SETTINGS, 40 | ICON_SWIRL, 41 | ICON_SWITCH, 42 | ICON_SYNC, 43 | ICON_TRASH, 44 | ICON_USER, 45 | ICON_WARNING, 46 | ICON_WEB, 47 | ) 48 | 49 | # Filter matching rules 50 | from .workflow import ( 51 | MATCH_ALL, 52 | MATCH_ALLCHARS, 53 | MATCH_ATOM, 54 | MATCH_CAPITALS, 55 | MATCH_INITIALS, 56 | MATCH_INITIALS_CONTAIN, 57 | MATCH_INITIALS_STARTSWITH, 58 | MATCH_STARTSWITH, 59 | MATCH_SUBSTRING, 60 | ) 61 | 62 | 63 | __title__ = 'Alfred-Workflow' 64 | __version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read() 65 | __author__ = 'Dean Jackson' 66 | __licence__ = 'MIT' 67 | __copyright__ = 'Copyright 2014-2019 Dean Jackson' 68 | 69 | __all__ = [ 70 | 'Variables', 71 | 'Workflow', 72 | 'Workflow3', 73 | 'manager', 74 | 'PasswordNotFound', 75 | 'KeychainError', 76 | 'ICON_ACCOUNT', 77 | 'ICON_BURN', 78 | 'ICON_CLOCK', 79 | 'ICON_COLOR', 80 | 'ICON_COLOUR', 81 | 'ICON_EJECT', 82 | 'ICON_ERROR', 83 | 'ICON_FAVORITE', 84 | 'ICON_FAVOURITE', 85 | 'ICON_GROUP', 86 | 'ICON_HELP', 87 | 'ICON_HOME', 88 | 'ICON_INFO', 89 | 'ICON_NETWORK', 90 | 'ICON_NOTE', 91 | 'ICON_SETTINGS', 92 | 'ICON_SWIRL', 93 | 'ICON_SWITCH', 94 | 'ICON_SYNC', 95 | 'ICON_TRASH', 96 | 'ICON_USER', 97 | 'ICON_WARNING', 98 | 'ICON_WEB', 99 | 'MATCH_ALL', 100 | 'MATCH_ALLCHARS', 101 | 'MATCH_ATOM', 102 | 'MATCH_CAPITALS', 103 | 'MATCH_INITIALS', 104 | 'MATCH_INITIALS_CONTAIN', 105 | 'MATCH_INITIALS_STARTSWITH', 106 | 'MATCH_STARTSWITH', 107 | 'MATCH_SUBSTRING', 108 | ] 109 | -------------------------------------------------------------------------------- /src/workflow/background.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2014 deanishe@deanishe.net 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2014-04-06 9 | # 10 | 11 | """This module provides an API to run commands in background processes. 12 | 13 | Combine with the :ref:`caching API ` to work from cached data 14 | while you fetch fresh data in the background. 15 | 16 | See :ref:`the User Manual ` for more information 17 | and examples. 18 | """ 19 | 20 | from __future__ import print_function, unicode_literals 21 | 22 | import signal 23 | import sys 24 | import os 25 | import subprocess 26 | import pickle 27 | 28 | from workflow import Workflow 29 | 30 | __all__ = ['is_running', 'run_in_background'] 31 | 32 | _wf = None 33 | 34 | 35 | def wf(): 36 | global _wf 37 | if _wf is None: 38 | _wf = Workflow() 39 | return _wf 40 | 41 | 42 | def _log(): 43 | return wf().logger 44 | 45 | 46 | def _arg_cache(name): 47 | """Return path to pickle cache file for arguments. 48 | 49 | :param name: name of task 50 | :type name: ``unicode`` 51 | :returns: Path to cache file 52 | :rtype: ``unicode`` filepath 53 | 54 | """ 55 | return wf().cachefile(name + '.argcache') 56 | 57 | 58 | def _pid_file(name): 59 | """Return path to PID file for ``name``. 60 | 61 | :param name: name of task 62 | :type name: ``unicode`` 63 | :returns: Path to PID file for task 64 | :rtype: ``unicode`` filepath 65 | 66 | """ 67 | return wf().cachefile(name + '.pid') 68 | 69 | 70 | def _process_exists(pid): 71 | """Check if a process with PID ``pid`` exists. 72 | 73 | :param pid: PID to check 74 | :type pid: ``int`` 75 | :returns: ``True`` if process exists, else ``False`` 76 | :rtype: ``Boolean`` 77 | 78 | """ 79 | try: 80 | os.kill(pid, 0) 81 | except OSError: # not running 82 | return False 83 | return True 84 | 85 | 86 | def _job_pid(name): 87 | """Get PID of job or `None` if job does not exist. 88 | 89 | Args: 90 | name (str): Name of job. 91 | 92 | Returns: 93 | int: PID of job process (or `None` if job doesn't exist). 94 | """ 95 | pidfile = _pid_file(name) 96 | if not os.path.exists(pidfile): 97 | return 98 | 99 | with open(pidfile, 'rb') as fp: 100 | pid = int(fp.read()) 101 | 102 | if _process_exists(pid): 103 | return pid 104 | 105 | os.unlink(pidfile) 106 | 107 | 108 | def is_running(name): 109 | """Test whether task ``name`` is currently running. 110 | 111 | :param name: name of task 112 | :type name: unicode 113 | :returns: ``True`` if task with name ``name`` is running, else ``False`` 114 | :rtype: bool 115 | 116 | """ 117 | if _job_pid(name) is not None: 118 | return True 119 | 120 | return False 121 | 122 | 123 | def _background(pidfile, stdin='/dev/null', stdout='/dev/null', 124 | stderr='/dev/null'): # pragma: no cover 125 | """Fork the current process into a background daemon. 126 | 127 | :param pidfile: file to write PID of daemon process to. 128 | :type pidfile: filepath 129 | :param stdin: where to read input 130 | :type stdin: filepath 131 | :param stdout: where to write stdout output 132 | :type stdout: filepath 133 | :param stderr: where to write stderr output 134 | :type stderr: filepath 135 | 136 | """ 137 | def _fork_and_exit_parent(errmsg, wait=False, write=False): 138 | try: 139 | pid = os.fork() 140 | if pid > 0: 141 | if write: # write PID of child process to `pidfile` 142 | tmp = pidfile + '.tmp' 143 | with open(tmp, 'wb') as fp: 144 | fp.write(str(pid)) 145 | os.rename(tmp, pidfile) 146 | if wait: # wait for child process to exit 147 | os.waitpid(pid, 0) 148 | os._exit(0) 149 | except OSError as err: 150 | _log().critical('%s: (%d) %s', errmsg, err.errno, err.strerror) 151 | raise err 152 | 153 | # Do first fork and wait for second fork to finish. 154 | _fork_and_exit_parent('fork #1 failed', wait=True) 155 | 156 | # Decouple from parent environment. 157 | os.chdir(wf().workflowdir) 158 | os.setsid() 159 | 160 | # Do second fork and write PID to pidfile. 161 | _fork_and_exit_parent('fork #2 failed', write=True) 162 | 163 | # Now I am a daemon! 164 | # Redirect standard file descriptors. 165 | si = open(stdin, 'r', 0) 166 | so = open(stdout, 'a+', 0) 167 | se = open(stderr, 'a+', 0) 168 | if hasattr(sys.stdin, 'fileno'): 169 | os.dup2(si.fileno(), sys.stdin.fileno()) 170 | if hasattr(sys.stdout, 'fileno'): 171 | os.dup2(so.fileno(), sys.stdout.fileno()) 172 | if hasattr(sys.stderr, 'fileno'): 173 | os.dup2(se.fileno(), sys.stderr.fileno()) 174 | 175 | 176 | def kill(name, sig=signal.SIGTERM): 177 | """Send a signal to job ``name`` via :func:`os.kill`. 178 | 179 | .. versionadded:: 1.29 180 | 181 | Args: 182 | name (str): Name of the job 183 | sig (int, optional): Signal to send (default: SIGTERM) 184 | 185 | Returns: 186 | bool: `False` if job isn't running, `True` if signal was sent. 187 | """ 188 | pid = _job_pid(name) 189 | if pid is None: 190 | return False 191 | 192 | os.kill(pid, sig) 193 | return True 194 | 195 | 196 | def run_in_background(name, args, **kwargs): 197 | r"""Cache arguments then call this script again via :func:`subprocess.call`. 198 | 199 | :param name: name of job 200 | :type name: unicode 201 | :param args: arguments passed as first argument to :func:`subprocess.call` 202 | :param \**kwargs: keyword arguments to :func:`subprocess.call` 203 | :returns: exit code of sub-process 204 | :rtype: int 205 | 206 | When you call this function, it caches its arguments and then calls 207 | ``background.py`` in a subprocess. The Python subprocess will load the 208 | cached arguments, fork into the background, and then run the command you 209 | specified. 210 | 211 | This function will return as soon as the ``background.py`` subprocess has 212 | forked, returning the exit code of *that* process (i.e. not of the command 213 | you're trying to run). 214 | 215 | If that process fails, an error will be written to the log file. 216 | 217 | If a process is already running under the same name, this function will 218 | return immediately and will not run the specified command. 219 | 220 | """ 221 | if is_running(name): 222 | _log().info('[%s] job already running', name) 223 | return 224 | 225 | argcache = _arg_cache(name) 226 | 227 | # Cache arguments 228 | with open(argcache, 'wb') as fp: 229 | pickle.dump({'args': args, 'kwargs': kwargs}, fp) 230 | _log().debug('[%s] command cached: %s', name, argcache) 231 | 232 | # Call this script 233 | cmd = ['/usr/bin/python', __file__, name] 234 | _log().debug('[%s] passing job to background runner: %r', name, cmd) 235 | retcode = subprocess.call(cmd) 236 | 237 | if retcode: # pragma: no cover 238 | _log().error('[%s] background runner failed with %d', name, retcode) 239 | else: 240 | _log().debug('[%s] background job started', name) 241 | 242 | return retcode 243 | 244 | 245 | def main(wf): # pragma: no cover 246 | """Run command in a background process. 247 | 248 | Load cached arguments, fork into background, then call 249 | :meth:`subprocess.call` with cached arguments. 250 | 251 | """ 252 | log = wf.logger 253 | name = wf.args[0] 254 | argcache = _arg_cache(name) 255 | if not os.path.exists(argcache): 256 | msg = '[{0}] command cache not found: {1}'.format(name, argcache) 257 | log.critical(msg) 258 | raise IOError(msg) 259 | 260 | # Fork to background and run command 261 | pidfile = _pid_file(name) 262 | _background(pidfile) 263 | 264 | # Load cached arguments 265 | with open(argcache, 'rb') as fp: 266 | data = pickle.load(fp) 267 | 268 | # Cached arguments 269 | args = data['args'] 270 | kwargs = data['kwargs'] 271 | 272 | # Delete argument cache file 273 | os.unlink(argcache) 274 | 275 | try: 276 | # Run the command 277 | log.debug('[%s] running command: %r', name, args) 278 | 279 | retcode = subprocess.call(args, **kwargs) 280 | 281 | if retcode: 282 | log.error('[%s] command failed with status %d', name, retcode) 283 | finally: 284 | os.unlink(pidfile) 285 | 286 | log.debug('[%s] job complete', name) 287 | 288 | 289 | if __name__ == '__main__': # pragma: no cover 290 | wf().run(main) 291 | -------------------------------------------------------------------------------- /src/workflow/version: -------------------------------------------------------------------------------- 1 | 1.40.0 --------------------------------------------------------------------------------