# 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"
}