unit-syntax


Nameunit-syntax JSON
Version 0.3.1 PyPI version JSON
download
home_pagehttps://github.com/ahupp/unit-syntax
SummaryPhysical unit literals for Jupyter and IPython
upload_time2023-08-16 21:00:38
maintainer
docs_urlNone
authorAdam Hupp
requires_python>=3.8,<4
license
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            `unit-syntax` adds support for physical units to the Python language:

```python
>>> speed = 5 meters/second
>>> (2 seconds) * speed
10 meter
```

Why? I like to use Python as a calculator for physical problems and wished it had the type safety of explicit units along with the readability of normal notation.

`unit-syntax` works in Jupyter notebooks, standalone Python scripts, and Python packages.

[How does it work?](https://github.com/ahupp/unit-syntax#how-does-it-work)

## Getting Started

Install the package:

```shell
$ pip install unit-syntax
```

### ... with Jupyter/IPython

To enable unit-syntax in a Jupyter/IPython session run:

```python
%load_ext unit_syntax
```

Tip: In Jupyter this must be run in its own cell before any units expressions are evaluated.

### ... with standalone scripts

To run a standalone script with units:

```
$ python -m unit_syntax <path_to_script.py>
```

Note that this installs a custom import hook that affects all imports performed by the script.

### ... with Python packages

To use/distribute a package with unit-syntax, add this in your `__init__.py`:

```python
from unit_syntax.import_hook import enable_units_for_package
enable_units_for_package(__name__)
```

This applies the transform only to sub-modules of your package.

## Usage

[An interactive notebook to play around with units](https://colab.research.google.com/drive/1PInyLGZHnUzEuUVgMsLrUUNdCurXK7v1#scrollTo=JszzXmATY0TV)

Units can be applied to any "simple" expression:

- number: `1 meter`
- variables: `x parsec`, `y.z watts`, `area[id] meters**2`
- lists and tuples: `[1., 37.] newton meters`
- unary operators: `-x dBm`
- power: `x**2 meters`

In expressions mixing units and binary operators, parenthesize:

```python
one_lux = (1 lumen)/(1 meter**2)
```

Units can be used in any place where Python allows expressions, e.g:

- function arguments: `area_of_circle(radius=1 meter)`
- list comprehensions: `[x meters for x in range(10)]`

Quantities can be converted to another measurement system:

```python
>>> (88 miles / hour) furlongs / fortnight
236543.5269120001 furlong / fortnight
>>> (0 degC) degF
31.999999999999936 degree_Fahrenheit
```

Compound units (e.g. `newtons/meter**2`) are supported and follow the usual precedence rules.

Units _may not_ begin with parentheses (consider the possible
interpretations of `x (meters)`). Parentheses are allowed anywhere else:

```python
# parsed as a function call, will result in a runtime error
x (newton meters)/(second*kg)
# a-ok
x newton meters/(second*kg)
```

Using unknown units produces a syntax error at import time:

```python
>>> 1 smoot
...
SyntaxError: 'smoot' is not defined in the unit registry
```

## How does it work?

`unit-syntax` transforms python-with-units into standard python that calls the excellent [pint](https://pint.readthedocs.io/en/stable/) units handling library.

The parser is [pegen](https://we-like-parsers.github.io/pegen/), which is a standalone version of the same parser generator used by Python itself. The grammar is a [lightly modified](https://github.com/ahupp/unit-syntax/compare/base-grammar..main#diff-7405fdc26614e4d2e7f8f37c9b559ccb3a7f7c619d41e207dda28afdfae20f83) version the official Python grammar shipped with pegen.

Syntax transformation in IPython/Jupyter uses [IPython custom input transformers](https://ipython.readthedocs.io/en/stable/config/inputtransforms.html).

Syntax transformation of arbitrary Python modules uses [importlib](https://docs.python.org/3/library/importlib.html)'s [MetaPathFinder](https://docs.python.org/3/library/importlib.html#importlib.abc.MetaPathFinder), see [import-transforms](https://github.com/ahupp/import-transformss) and [unit_syntax.import_hook](https://github.com/ahupp/unit-syntax/blob/main/unit_syntax/import_hook.py) for details.

## Why only allow units on simple expressions?

Imagine units were instead parsed as operator with high precedence and you wrote this reasonable looking expression:

```python
ppi = 300 pixels/inch
y = x inches * ppi
```

`inches * ppi` would be parsed as the unit, leading to (at best) a runtime error sometime later and at worst an incorrect calculation. This could be avoided by parenthesizing the expression (e.g. `(x inches) * ppi`, but if that's optional it's easy to forget. So the intent of this restriction is to make these risky forms uncommon and thus more obvious. This is not a hypothetical concern, I hit this within 10 minutes of first using the initial syntax.

## Prior Art

The immediate inspriration of `unit-syntax` is a language called [Fortress](https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.180.6323&rep=rep1&type=pdf) from Sun Microsystems. Fortress was intended as a modern Fortran, and had first-class support for units in both the syntax and type system.

F# (an OCaml derivative from Microsoft) also [has first class support for units](https://en.wikibooks.org/wiki/F_Sharp_Programming/Units_of_Measure).

The Julia package [Unitful.jl](http://painterqubits.github.io/Unitful.jl/stable/)

A [long discussion on the python-ideas mailing list](https://lwn.net/Articles/900739/) about literal units in Python.

## Development

To regenerate the parser:

`python -m pegen python_units.gram -o unit_syntax/parser.py`

Running tests:

```
$ poetry install --with dev
$ poetry run pytest
```

## Future work and open questions

- Test against various ipython and python versions
- Ensure bytecode caching still works
- Test with wider range of source files with the wildcard loader
- Unit type hints, maybe checked with [@runtime_checkable](https://docs.python.org/3/library/typing.html#typing.runtime_checkable). More Pint typechecking [discussion](https://github.com/hgrecco/pint/issues/1166)
- Typography of output
- pre-parse units
- talk to pint about interop between UnitRegistries
- Fix reported location of SyntaxError when paren missing


            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/ahupp/unit-syntax",
    "name": "unit-syntax",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.8,<4",
    "maintainer_email": "",
    "keywords": "",
    "author": "Adam Hupp",
    "author_email": "adam@hupp.org",
    "download_url": "https://files.pythonhosted.org/packages/70/31/d0873c6726ef4c34950866643e5b22b9ed261506f75e7acc8f453a7e4b36/unit_syntax-0.3.1.tar.gz",
    "platform": null,
    "description": "`unit-syntax` adds support for physical units to the Python language:\n\n```python\n>>> speed = 5 meters/second\n>>> (2 seconds) * speed\n10 meter\n```\n\nWhy? I like to use Python as a calculator for physical problems and wished it had the type safety of explicit units along with the readability of normal notation.\n\n`unit-syntax` works in Jupyter notebooks, standalone Python scripts, and Python packages.\n\n[How does it work?](https://github.com/ahupp/unit-syntax#how-does-it-work)\n\n## Getting Started\n\nInstall the package:\n\n```shell\n$ pip install unit-syntax\n```\n\n### ... with Jupyter/IPython\n\nTo enable unit-syntax in a Jupyter/IPython session run:\n\n```python\n%load_ext unit_syntax\n```\n\nTip: In Jupyter this must be run in its own cell before any units expressions are evaluated.\n\n### ... with standalone scripts\n\nTo run a standalone script with units:\n\n```\n$ python -m unit_syntax <path_to_script.py>\n```\n\nNote that this installs a custom import hook that affects all imports performed by the script.\n\n### ... with Python packages\n\nTo use/distribute a package with unit-syntax, add this in your `__init__.py`:\n\n```python\nfrom unit_syntax.import_hook import enable_units_for_package\nenable_units_for_package(__name__)\n```\n\nThis applies the transform only to sub-modules of your package.\n\n## Usage\n\n[An interactive notebook to play around with units](https://colab.research.google.com/drive/1PInyLGZHnUzEuUVgMsLrUUNdCurXK7v1#scrollTo=JszzXmATY0TV)\n\nUnits can be applied to any \"simple\" expression:\n\n- number: `1 meter`\n- variables: `x parsec`, `y.z watts`, `area[id] meters**2`\n- lists and tuples: `[1., 37.] newton meters`\n- unary operators: `-x dBm`\n- power: `x**2 meters`\n\nIn expressions mixing units and binary operators, parenthesize:\n\n```python\none_lux = (1 lumen)/(1 meter**2)\n```\n\nUnits can be used in any place where Python allows expressions, e.g:\n\n- function arguments: `area_of_circle(radius=1 meter)`\n- list comprehensions: `[x meters for x in range(10)]`\n\nQuantities can be converted to another measurement system:\n\n```python\n>>> (88 miles / hour) furlongs / fortnight\n236543.5269120001 furlong / fortnight\n>>> (0 degC) degF\n31.999999999999936 degree_Fahrenheit\n```\n\nCompound units (e.g. `newtons/meter**2`) are supported and follow the usual precedence rules.\n\nUnits _may not_ begin with parentheses (consider the possible\ninterpretations of `x (meters)`). Parentheses are allowed anywhere else:\n\n```python\n# parsed as a function call, will result in a runtime error\nx (newton meters)/(second*kg)\n# a-ok\nx newton meters/(second*kg)\n```\n\nUsing unknown units produces a syntax error at import time:\n\n```python\n>>> 1 smoot\n...\nSyntaxError: 'smoot' is not defined in the unit registry\n```\n\n## How does it work?\n\n`unit-syntax` transforms python-with-units into standard python that calls the excellent [pint](https://pint.readthedocs.io/en/stable/) units handling library.\n\nThe parser is [pegen](https://we-like-parsers.github.io/pegen/), which is a standalone version of the same parser generator used by Python itself. The grammar is a [lightly modified](https://github.com/ahupp/unit-syntax/compare/base-grammar..main#diff-7405fdc26614e4d2e7f8f37c9b559ccb3a7f7c619d41e207dda28afdfae20f83) version the official Python grammar shipped with pegen.\n\nSyntax transformation in IPython/Jupyter uses [IPython custom input transformers](https://ipython.readthedocs.io/en/stable/config/inputtransforms.html).\n\nSyntax transformation of arbitrary Python modules uses [importlib](https://docs.python.org/3/library/importlib.html)'s [MetaPathFinder](https://docs.python.org/3/library/importlib.html#importlib.abc.MetaPathFinder), see [import-transforms](https://github.com/ahupp/import-transformss) and [unit_syntax.import_hook](https://github.com/ahupp/unit-syntax/blob/main/unit_syntax/import_hook.py) for details.\n\n## Why only allow units on simple expressions?\n\nImagine units were instead parsed as operator with high precedence and you wrote this reasonable looking expression:\n\n```python\nppi = 300 pixels/inch\ny = x inches * ppi\n```\n\n`inches * ppi` would be parsed as the unit, leading to (at best) a runtime error sometime later and at worst an incorrect calculation. This could be avoided by parenthesizing the expression (e.g. `(x inches) * ppi`, but if that's optional it's easy to forget. So the intent of this restriction is to make these risky forms uncommon and thus more obvious. This is not a hypothetical concern, I hit this within 10 minutes of first using the initial syntax.\n\n## Prior Art\n\nThe immediate inspriration of `unit-syntax` is a language called [Fortress](https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.180.6323&rep=rep1&type=pdf) from Sun Microsystems. Fortress was intended as a modern Fortran, and had first-class support for units in both the syntax and type system.\n\nF# (an OCaml derivative from Microsoft) also [has first class support for units](https://en.wikibooks.org/wiki/F_Sharp_Programming/Units_of_Measure).\n\nThe Julia package [Unitful.jl](http://painterqubits.github.io/Unitful.jl/stable/)\n\nA [long discussion on the python-ideas mailing list](https://lwn.net/Articles/900739/) about literal units in Python.\n\n## Development\n\nTo regenerate the parser:\n\n`python -m pegen python_units.gram -o unit_syntax/parser.py`\n\nRunning tests:\n\n```\n$ poetry install --with dev\n$ poetry run pytest\n```\n\n## Future work and open questions\n\n- Test against various ipython and python versions\n- Ensure bytecode caching still works\n- Test with wider range of source files with the wildcard loader\n- Unit type hints, maybe checked with [@runtime_checkable](https://docs.python.org/3/library/typing.html#typing.runtime_checkable). More Pint typechecking [discussion](https://github.com/hgrecco/pint/issues/1166)\n- Typography of output\n- pre-parse units\n- talk to pint about interop between UnitRegistries\n- Fix reported location of SyntaxError when paren missing\n\n",
    "bugtrack_url": null,
    "license": "",
    "summary": "Physical unit literals for Jupyter and IPython",
    "version": "0.3.1",
    "project_urls": {
        "Homepage": "https://github.com/ahupp/unit-syntax",
        "Repository": "https://github.com/ahupp/unit-syntax"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "e1cba329c1dc89ff1a3fdf5a450b444c5ca1de40767179c3d08670dfc64bbbab",
                "md5": "ba5adb43aa8da75fb105bac11162f56b",
                "sha256": "64911364c098be7dbaeb15c51ff8b5a228ee333a6227a4f7854d433cc4a85458"
            },
            "downloads": -1,
            "filename": "unit_syntax-0.3.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "ba5adb43aa8da75fb105bac11162f56b",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8,<4",
            "size": 35837,
            "upload_time": "2023-08-16T21:00:37",
            "upload_time_iso_8601": "2023-08-16T21:00:37.252288Z",
            "url": "https://files.pythonhosted.org/packages/e1/cb/a329c1dc89ff1a3fdf5a450b444c5ca1de40767179c3d08670dfc64bbbab/unit_syntax-0.3.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "7031d0873c6726ef4c34950866643e5b22b9ed261506f75e7acc8f453a7e4b36",
                "md5": "1c87930037ab326ec7f1620bc9d1e3f9",
                "sha256": "b6825b4cc01ebd60cc97866294d8f8ec6a941dea1b360908c0eefc8c1a825e6f"
            },
            "downloads": -1,
            "filename": "unit_syntax-0.3.1.tar.gz",
            "has_sig": false,
            "md5_digest": "1c87930037ab326ec7f1620bc9d1e3f9",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8,<4",
            "size": 36650,
            "upload_time": "2023-08-16T21:00:38",
            "upload_time_iso_8601": "2023-08-16T21:00:38.454932Z",
            "url": "https://files.pythonhosted.org/packages/70/31/d0873c6726ef4c34950866643e5b22b9ed261506f75e7acc8f453a7e4b36/unit_syntax-0.3.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-08-16 21:00:38",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "ahupp",
    "github_project": "unit-syntax",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "unit-syntax"
}
        
Elapsed time: 0.10523s