newertype


Namenewertype JSON
Version 0.1.2 PyPI version JSON
download
home_pagehttps://github.com/evanjpw/newertype
SummaryAn Implementation of the NewType Pattern for Python that works in dynamic contexts.
upload_time2022-12-21 16:09:49
maintainer
docs_urlNone
authorEvan Williams
requires_python>=3.8,<4.0
licenseMIT
keywords python typechecker typing typehints runtime
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # newertype

An Implementation of the NewType Pattern for Python that works in dynamic contexts.

## What is it?

`NewerType` is a package that provides a semi-transparent wrapper to an existing type that allows it to be used
mostly as if it's just the wrapped type, but which allows type checking as if it's a distinct type at runtime.

With the addition to Python of [PEP 483](https://peps.python.org/pep-0483/),
[PEP 484](https://peps.python.org/pep-0484/), & the
[typing](https://docs.python.org/3/library/typing.html#module-typing) package, Python added support for type
hints. That included an implementation of the Haskell [`newtype`](https://wiki.haskell.org/Newtype) which was
cleverly called `NewType`.
As explained in [the documentation](https://docs.python.org/3/library/typing.html#typing.NewType),
Python's `NewType` is, like most of the
typing library, meant for use by static type checkers. This means that, when the code is running, the _Newness_ of
the type is erased, leaving just the wrapped type & no way to tell that there was ever a `Newtype`, either by
the code or by Python itself.

`NewerType` provides the same kind of wrapper as `NewType`, but allows (& enforces) type checking at runtime.
this means, for example, that if you wrap an `int` in a `NewerType`, you can do all of the arithmetic &
comparison operations on an instance of that type that you could with a normal `int` with either different
instances of that type, or `int`s. But you will not be able to mix _different_ `NewerType`s, even if they
all wrap `int`s.

This allows you to never have to worry if you are adding `Miles` to `Kilometers`, or mixing up a `UserName`
with a `Password`.

### Main Features

* A wrapper that allows dynamic type checking while mostly not getting in the way
* Carries type information with the object so you can always use `isinstace()` or `type()` to know what it is
* Forwards the magic methods from the wrapped object so things like arithmetic or indexing work
* Allows you to customize what methods are forwarded
* No dependencies!

## Installation

Current stable version:
```shell
pip install newertype
```

Newest thing on GitHub:
```shell
pip install git+https://github.com/evanjpw/newertype.git
```

## Usage

Basic usage:

```python
from newertype import NewerType

AType = NewerType("AType", int)  # `AType` is a new type that wraps an int
a_type = AType(14)  # Make an instance of this new type
isinstance(a_type, AType)  # `a_type` is an `AType`
# Returns: True
isinstance(a_type, int)  # `a_type` is _NOT_ an `int`
# Returns: False
str(a_type.__class__.__name__) == "AType"
# Returns: True
```

You can use the new type as if it's the wrapped type:

```python
AType = NewerType("AType", int)  # Let's make some types!
a_type_1 = AType(7)
a_type_2 = AType(7)  # Two different instances with the same class
a_type_1 == a_type_2  # You can compare them as if they were just `int`s
# Returns: True

EType = NewerType("EType", int)
e_type_1 = EType(7)
e_type_2 = EType(14)
e_type_2 > e_type_1  # All of the `int` operations work
# Returns: True
a_type_1 == e_type_1  # But different types are not equal, even if the wrapped value is
Returns: False

IType = NewerType("IType", int)
JType = NewerType("JType", int)
i_type_1 = IType(7)
i_type_2 = IType(14)
i_type_1 + i_type_2  # Arithmetic works!
# Returns: 21

j_type_1 = JType(7)
i_type_1 + j_type_1  # But not if you try to mix `NewerType`s
# "TypeError: unsupported operand type(s) for +: 'IType' and 'JType'"
int(i_type_1) < int(i_type_2)  # Conversions that work for the inner type work also
# Returns: True
```

Accessing the wrapped data directly:

```python
a_type = AType(14)
a_type.inner  # the `inner` property gets the contained value
# Returns: 14
a_type.inner = 27  # `inner` can also be used to modify the value
a_type.inner
# Returns: 27
```

The "truthiness" & string representations are sensible:

```python
SType = NewerType("SType", float)
s_type = SType(2.71828182845904523536028747135266249775724709369995)
str(s_type)
# Returns: "SType(2.718281828459045)"
repr(s_type)
# Returns: "SType(2.718281828459045)"
bool(s_type)
# Returns: True
bytes(s_type)  # `bytes()` only works if it works with the wrapped type
# "TypeError: cannot convert 'float' object to bytes"

s_type.inner = 0.0
bool(s_type)
# Returns: False
```

What about forwarding your own methods on your own classes? NewerType can handle that:

```python
# First, define a class. It can have the standard indexing methods, but also some unique ones:
class Forwardable(UserDict):
    def forwarded(self, value):
        return value

    def also_forwarded(self, key):
        return self[key]

    def __getitem__(self, item):
        return super().__getitem__(item)

    def __setitem__(self, key, value):
        super().__setitem__(key, value)

# The normal behavior is for NewerType to forward the standard methods but ignore the custom ones:
FO1Type = NewerType("FO1Type", Forwardable)
fo1_type_1 = FO1Type(Forwardable())
fo1_type_1["a"] = 5  # `__setitem__` is a standard method, so it's forwarded
fo1_type_1["a"]  # So is `__getitem__`
# Returns: 5
fo1_type_1.forwarded(5)  # But unique methods are not forwarded
# "AttributeError: FO1Type' object has no attribute 'forwarded'"

# We can use "extra_forwards" to specify the additional methods we'd like to forward:
FO2Type = NewerType(
    "FO2Type", Forwardable, extra_forwards=["forwarded", "also_forwarded"]
)
fo2_type_1 = FO2Type(Forwardable())
fo2_type_1["e"] = 7  # This continues to work
fo2_type_1["e"]  # As does this
# Returns: 7
fo2_type_1.also_forwarded("e")  # But now this works also!
# Returns: 7

# But what if we _don't_ want to forward the standard methods? Use "no_def_forwards":
FO3Type = NewerType(
    "FO3Type", Forwardable, extra_forwards=["also_forwarded"], no_def_forwards=True
)
fo3_type_1 = FO3Type(Forwardable())
fo3_type_1.inner["g"] = 8
fo3_type_1.also_forwarded("g")  # The extra methods continue to work
# Returns: 8
fo3_type_1["g"]  # But the standard ones don't (unless we specifically mention them in "extra_forwards")
# "TypeError: 'FO3Type' object is not subscriptable"
```

## TBD

* The `bytes()` built-in currently just forces all wrapped `str` objects to "utf-8" as an encoding.
 If you need a *different* encoding, use `bytes()` of `.inner`.
* There are a *bunch* more methods that should be in the whitelist for forwarding. That's a work in progress.

## Project Resources

* Documentation - TBD
* Issue tracker - TBD
* Source code - TBD
* Change log - TBD

## License

Licensed under the [MIT LICENSE](https://www.mit.edu/~amini/LICENSE.md)

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/evanjpw/newertype",
    "name": "newertype",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.8,<4.0",
    "maintainer_email": "",
    "keywords": "python,typechecker,typing,typehints,runtime",
    "author": "Evan Williams",
    "author_email": "ejw@fig.com",
    "download_url": "https://files.pythonhosted.org/packages/58/ca/a89e59b8c7a2749df90326d6899e002e56d003652f02c560c72d313dc9dc/newertype-0.1.2.tar.gz",
    "platform": null,
    "description": "# newertype\n\nAn Implementation of the NewType Pattern for Python that works in dynamic contexts.\n\n## What is it?\n\n`NewerType` is a package that provides a semi-transparent wrapper to an existing type that allows it to be used\nmostly as if it's just the wrapped type, but which allows type checking as if it's a distinct type at runtime.\n\nWith the addition to Python of [PEP 483](https://peps.python.org/pep-0483/),\n[PEP 484](https://peps.python.org/pep-0484/), & the\n[typing](https://docs.python.org/3/library/typing.html#module-typing) package, Python added support for type\nhints. That included an implementation of the Haskell [`newtype`](https://wiki.haskell.org/Newtype) which was\ncleverly called `NewType`.\nAs explained in [the documentation](https://docs.python.org/3/library/typing.html#typing.NewType),\nPython's `NewType` is, like most of the\ntyping library, meant for use by static type checkers. This means that, when the code is running, the _Newness_ of\nthe type is erased, leaving just the wrapped type & no way to tell that there was ever a `Newtype`, either by\nthe code or by Python itself.\n\n`NewerType` provides the same kind of wrapper as `NewType`, but allows (& enforces) type checking at runtime.\nthis means, for example, that if you wrap an `int` in a `NewerType`, you can do all of the arithmetic &\ncomparison operations on an instance of that type that you could with a normal `int` with either different\ninstances of that type, or `int`s. But you will not be able to mix _different_ `NewerType`s, even if they\nall wrap `int`s.\n\nThis allows you to never have to worry if you are adding `Miles` to `Kilometers`, or mixing up a `UserName`\nwith a `Password`.\n\n### Main Features\n\n* A wrapper that allows dynamic type checking while mostly not getting in the way\n* Carries type information with the object so you can always use `isinstace()` or `type()` to know what it is\n* Forwards the magic methods from the wrapped object so things like arithmetic or indexing work\n* Allows you to customize what methods are forwarded\n* No dependencies!\n\n## Installation\n\nCurrent stable version:\n```shell\npip install newertype\n```\n\nNewest thing on GitHub:\n```shell\npip install git+https://github.com/evanjpw/newertype.git\n```\n\n## Usage\n\nBasic usage:\n\n```python\nfrom newertype import NewerType\n\nAType = NewerType(\"AType\", int)  # `AType` is a new type that wraps an int\na_type = AType(14)  # Make an instance of this new type\nisinstance(a_type, AType)  # `a_type` is an `AType`\n# Returns: True\nisinstance(a_type, int)  # `a_type` is _NOT_ an `int`\n# Returns: False\nstr(a_type.__class__.__name__) == \"AType\"\n# Returns: True\n```\n\nYou can use the new type as if it's the wrapped type:\n\n```python\nAType = NewerType(\"AType\", int)  # Let's make some types!\na_type_1 = AType(7)\na_type_2 = AType(7)  # Two different instances with the same class\na_type_1 == a_type_2  # You can compare them as if they were just `int`s\n# Returns: True\n\nEType = NewerType(\"EType\", int)\ne_type_1 = EType(7)\ne_type_2 = EType(14)\ne_type_2 > e_type_1  # All of the `int` operations work\n# Returns: True\na_type_1 == e_type_1  # But different types are not equal, even if the wrapped value is\nReturns: False\n\nIType = NewerType(\"IType\", int)\nJType = NewerType(\"JType\", int)\ni_type_1 = IType(7)\ni_type_2 = IType(14)\ni_type_1 + i_type_2  # Arithmetic works!\n# Returns: 21\n\nj_type_1 = JType(7)\ni_type_1 + j_type_1  # But not if you try to mix `NewerType`s\n# \"TypeError: unsupported operand type(s) for +: 'IType' and 'JType'\"\nint(i_type_1) < int(i_type_2)  # Conversions that work for the inner type work also\n# Returns: True\n```\n\nAccessing the wrapped data directly:\n\n```python\na_type = AType(14)\na_type.inner  # the `inner` property gets the contained value\n# Returns: 14\na_type.inner = 27  # `inner` can also be used to modify the value\na_type.inner\n# Returns: 27\n```\n\nThe \"truthiness\" & string representations are sensible:\n\n```python\nSType = NewerType(\"SType\", float)\ns_type = SType(2.71828182845904523536028747135266249775724709369995)\nstr(s_type)\n# Returns: \"SType(2.718281828459045)\"\nrepr(s_type)\n# Returns: \"SType(2.718281828459045)\"\nbool(s_type)\n# Returns: True\nbytes(s_type)  # `bytes()` only works if it works with the wrapped type\n# \"TypeError: cannot convert 'float' object to bytes\"\n\ns_type.inner = 0.0\nbool(s_type)\n# Returns: False\n```\n\nWhat about forwarding your own methods on your own classes? NewerType can handle that:\n\n```python\n# First, define a class. It can have the standard indexing methods, but also some unique ones:\nclass Forwardable(UserDict):\n    def forwarded(self, value):\n        return value\n\n    def also_forwarded(self, key):\n        return self[key]\n\n    def __getitem__(self, item):\n        return super().__getitem__(item)\n\n    def __setitem__(self, key, value):\n        super().__setitem__(key, value)\n\n# The normal behavior is for NewerType to forward the standard methods but ignore the custom ones:\nFO1Type = NewerType(\"FO1Type\", Forwardable)\nfo1_type_1 = FO1Type(Forwardable())\nfo1_type_1[\"a\"] = 5  # `__setitem__` is a standard method, so it's forwarded\nfo1_type_1[\"a\"]  # So is `__getitem__`\n# Returns: 5\nfo1_type_1.forwarded(5)  # But unique methods are not forwarded\n# \"AttributeError: FO1Type' object has no attribute 'forwarded'\"\n\n# We can use \"extra_forwards\" to specify the additional methods we'd like to forward:\nFO2Type = NewerType(\n    \"FO2Type\", Forwardable, extra_forwards=[\"forwarded\", \"also_forwarded\"]\n)\nfo2_type_1 = FO2Type(Forwardable())\nfo2_type_1[\"e\"] = 7  # This continues to work\nfo2_type_1[\"e\"]  # As does this\n# Returns: 7\nfo2_type_1.also_forwarded(\"e\")  # But now this works also!\n# Returns: 7\n\n# But what if we _don't_ want to forward the standard methods? Use \"no_def_forwards\":\nFO3Type = NewerType(\n    \"FO3Type\", Forwardable, extra_forwards=[\"also_forwarded\"], no_def_forwards=True\n)\nfo3_type_1 = FO3Type(Forwardable())\nfo3_type_1.inner[\"g\"] = 8\nfo3_type_1.also_forwarded(\"g\")  # The extra methods continue to work\n# Returns: 8\nfo3_type_1[\"g\"]  # But the standard ones don't (unless we specifically mention them in \"extra_forwards\")\n# \"TypeError: 'FO3Type' object is not subscriptable\"\n```\n\n## TBD\n\n* The `bytes()` built-in currently just forces all wrapped `str` objects to \"utf-8\" as an encoding.\n If you need a *different* encoding, use `bytes()` of `.inner`.\n* There are a *bunch* more methods that should be in the whitelist for forwarding. That's a work in progress.\n\n## Project Resources\n\n* Documentation - TBD\n* Issue tracker - TBD\n* Source code - TBD\n* Change log - TBD\n\n## License\n\nLicensed under the [MIT LICENSE](https://www.mit.edu/~amini/LICENSE.md)\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "An Implementation of the NewType Pattern for Python that works in dynamic contexts.",
    "version": "0.1.2",
    "split_keywords": [
        "python",
        "typechecker",
        "typing",
        "typehints",
        "runtime"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "md5": "b2bb571062f776752a44bbe9e7b762e1",
                "sha256": "b4322dd10795ff92705dde3c47a9516b394a20edfe6ae98f0f9b9e7f8dfc643d"
            },
            "downloads": -1,
            "filename": "newertype-0.1.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "b2bb571062f776752a44bbe9e7b762e1",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8,<4.0",
            "size": 6854,
            "upload_time": "2022-12-21T16:09:48",
            "upload_time_iso_8601": "2022-12-21T16:09:48.369660Z",
            "url": "https://files.pythonhosted.org/packages/e6/6c/d22e00a4be91457aca6facc2a8d9689cbde1aa89c239583c81546efb259b/newertype-0.1.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "76ead8b39553e1bfa705217a9250a620",
                "sha256": "bc401b792db609f74bd142da3544936861a6072d84ccd39c8c3985a2a0cae0fb"
            },
            "downloads": -1,
            "filename": "newertype-0.1.2.tar.gz",
            "has_sig": false,
            "md5_digest": "76ead8b39553e1bfa705217a9250a620",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8,<4.0",
            "size": 6781,
            "upload_time": "2022-12-21T16:09:49",
            "upload_time_iso_8601": "2022-12-21T16:09:49.928938Z",
            "url": "https://files.pythonhosted.org/packages/58/ca/a89e59b8c7a2749df90326d6899e002e56d003652f02c560c72d313dc9dc/newertype-0.1.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2022-12-21 16:09:49",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "github_user": "evanjpw",
    "github_project": "newertype",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "newertype"
}
        
Elapsed time: 0.02056s