Name | encrustable JSON |
Version |
1.0.0.post2
JSON |
| download |
home_page | None |
Summary | Basic components of Rust's type system magic in Python |
upload_time | 2025-08-08 20:16:05 |
maintainer | None |
docs_url | None |
author | Artur Ciesielski |
requires_python | <4.0,>=3.12 |
license | GPL-3.0-or-later |
keywords |
|
VCS |
|
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
<!-- `encrustable` - basic components of Rust's type system magic
Copyright (C) 2025 Artur Ciesielski <artur.ciesielski@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. -->
# `encrustable`
[](https://gitlab.com/arcanery/python/encrustable/-/commits/main)
[](https://gitlab.com/arcanery/python/encrustable/-/commits/main)
[](https://gitlab.com/arcanery/python/encrustable/-/releases)
## Introduction
`encrustable` is a small project that aims to bring the very basic benefits
of the Rust's type system to Python. With the introduction of pattern matching
in Python 3.12 and all the benefits associated with relevant type checking
this paradigm becomes more and more usable.
Currently the main focus of `encrustable` is to port over `Option` and
`Result` types, the very basic building blocks of Rust's type safety.
## Using `encrustable`
### Installation
```bash
python -m pip install encrustable
```
### Basic usage of `Option`
Run the example with `python -m encrustable.examples.option`:
```python
from typing import NewType
from encrustable import Nothing, Option, Some
# we use the newtype pattern to squeeze the most ouf of our type checker
Username = NewType("Username", str)
PasswdFileLine = NewType("PasswdFileLine", str)
ParsedPasswdEntry = NewType("ParsedPasswdEntry", list[str])
ShellPath = NewType("ShellPath", str)
# we assume the passwd file exists, is readable and consists of valid lines, so we have
# a function that can fail in exactly one way (the user is not in the passwd file)
# we encode this scenario as an Option
def get_user_entry(username: Username) -> Option[PasswdFileLine]:
with open("/etc/passwd", "r") as passwd_file:
for line in passwd_file:
parts: list[str] = line.split(":")
if parts[0] == username:
return Some(PasswdFileLine(line.strip()))
return Nothing()
# we have a regular function that cannot fail
# assuming it receives a valid passwd file line
def parse_user_entry(entry: PasswdFileLine) -> ParsedPasswdEntry:
return ParsedPasswdEntry(entry.split(":"))
# we have another regular function that cannot fail
# assuming it receives a valid parsed passwd entry
def get_shell_name(parsed_entry: ParsedPasswdEntry) -> ShellPath:
return ShellPath(parsed_entry[6])
# now we can combine them and we can skip any error checking midway
# we will only check the result at the end using pattern matching
def print_user_shell(username: Username) -> None:
match get_user_entry(username) | parse_user_entry | get_shell_name:
# the result is of the type `Option[ShellPath]`
case Some(s):
print(f"- user '{username}' has shell set to '{s}'")
case Nothing():
print(f"- user '{username}' not found in the '/etc/passwd' file")
# execute the example for two users, "root" and "anchovies"
if __name__ == "__main__":
print_user_shell(Username("root"))
print_user_shell(Username("anchovies"))
```
```
- user 'root' has shell set to '/bin/bash'
- user 'anchovies' not found in the '/etc/passwd' file
```
### Basic usage of `Result`
Run the example with `python -m encrustable.examples.result`:
```python
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from encrustable import Err, Ok, Result
@dataclass(frozen=True, eq=True)
class FileReadError(Exception):
file_name: Path
original_error: Exception | None = field(default=None, kw_only=True, repr=False)
class FileNotFound(FileReadError):
pass
class FileNotReadable(FileReadError):
pass
class FileEmpty(FileReadError):
pass
# this function can fail in many different ways:
# - file does not exist
# - file is not readable (because of permissions)
# - file is empty (so no first line)
# - we can even have a catch-all generic exception handler if we want to
# we encode this scenario as a Result
def read_first_line_from_file(file_name: Path) -> Result[str, FileReadError]:
try:
with file_name.open("r") as file:
if (line := file.readline().strip()) == "":
return Err(FileEmpty(file_name))
return Ok(line)
except FileNotFoundError as e:
return Err(FileNotFound(file_name, original_error=e))
except PermissionError as e:
return Err(FileNotReadable(file_name, original_error=e))
except Exception as e:
return Err(FileReadError(file_name, original_error=e))
# we have a regular function that cannot fail
# given a valid string it will give us up to 20 characters from the beginning
def trim_str_above_20_len(line: str) -> str:
return line.strip()[:20]
# now we can combine them and we can skip any error checking midway
# we will only check the result at the end using pattern matching
def print_first_line_from_file(file_name: Path) -> None:
match read_first_line_from_file(file_name) | trim_str_above_20_len:
case Ok(line):
print(f"- First line (up to 20 chars) from '{file_name}': {line}")
case Err(err):
print(
f"- Could not read first line from '{str(file_name)[:20]}'\n"
f" error: {repr(err)}\n"
f" original error: {repr(err.original_error)}"
)
# execute the example for a couple of filenames
if __name__ == "__main__":
print_first_line_from_file(Path("/etc/passwd"))
print_first_line_from_file(Path("/does-not-exist.yaml"))
print_first_line_from_file(Path("/root/file.yaml"))
with tempfile.NamedTemporaryFile("w") as f:
print_first_line_from_file(Path(f.name))
```
```
- First line (up to 20 chars) from '/etc/passwd': root:x:0:0:root:/roo
- Could not read first line from '/does-not-exist.yaml'
error: FileNotFound(file_name=PosixPath('/does-not-exist.yaml'))
original error: [Errno 2] No such file or directory: '/does-not-exist.yaml'
- Could not read first line from '/root/file.yaml'
error: FileNotReadable(file_name=PosixPath('/root/file.yaml'))
original error: [Errno 13] Permission denied: '/root/file.yaml'
- Could not read first line from '/tmp/tmp1jbyntjo'
error: FileEmpty(file_name=PosixPath('/tmp/tmp1jbyntjo'))
original error: None
```
### The `Panic` boundary
The boundary between `Option`s/`Result`s and regular Python code can be force-crossed
by using the `unwrap*` or `expect*` methods, which immediately extracts the contained
value. This can however raise a `Panic` if used when the object state is not the one
that's expected, so pattern matching should be the preferred way of extracting values.
`Panic`s are normal Python `Exception`s and can be caught at the boundary.
Run the example with `python -m encrustable.examples.panic`:
```python
from encrustable import Err, Nothing, Ok, Option, Panic, Result, Some
def get_some(v: int) -> Option[int]:
return Some(v)
def get_nothing() -> Option[int]:
return Nothing()
def get_ok(v: int) -> Result[int, Exception]:
return Ok(v)
def get_err(e: Exception) -> Result[int, Exception]:
return Err(e)
def try_unwrap[T, E: Exception](v: Option[T] | Result[T, E]) -> None:
try:
unwrapped = v.unwrap()
print(f"- {repr(v)} unwrap: {repr(unwrapped)}")
except Panic as p:
print(f"- {repr(v)} unwrap: {str(p)}\n cause: {repr(p.__cause__)}")
def try_expect[T, E: Exception](v: Option[T] | Result[T, E]) -> None:
try:
unwrapped = v.expect("no valid value")
print(f"- {repr(v)} expect: {repr(unwrapped)}")
except Panic as p:
print(f"- {repr(v)} expect: {str(p)}\n cause: {repr(p.__cause__)}")
if __name__ == "__main__":
try_unwrap(get_some(1))
try_expect(get_some(1))
try_unwrap(get_nothing())
try_expect(get_nothing())
try_unwrap(get_ok(1))
try_expect(get_ok(1))
try_unwrap(get_err(Exception()))
try_expect(get_err(Exception()))
```
```
- Some(v=1) unwrap: 1
- Some(v=1) expect: 1
- Nothing() unwrap: panic!
cause: None
- Nothing() expect: no valid value
cause: None
- Ok(v=1) unwrap: 1
- Ok(v=1) expect: 1
- Err(e=Exception()) unwrap: panic!
cause: Exception()
- Err(e=Exception()) expect: no valid value
cause: Exception()
```
Raw data
{
"_id": null,
"home_page": null,
"name": "encrustable",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.12",
"maintainer_email": null,
"keywords": null,
"author": "Artur Ciesielski",
"author_email": "artur.ciesielski@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/9f/ea/d90de1032f034ce2a63936b2b550c2bbcd3f3439706d0271e50c850c9c06/encrustable-1.0.0.post2.tar.gz",
"platform": null,
"description": "<!-- `encrustable` - basic components of Rust's type system magic\nCopyright (C) 2025 Artur Ciesielski <artur.ciesielski@gmail.com>\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program. If not, see <https://www.gnu.org/licenses/>. -->\n\n# `encrustable`\n\n[](https://gitlab.com/arcanery/python/encrustable/-/commits/main)\n[](https://gitlab.com/arcanery/python/encrustable/-/commits/main)\n[](https://gitlab.com/arcanery/python/encrustable/-/releases)\n\n## Introduction\n\n`encrustable` is a small project that aims to bring the very basic benefits\nof the Rust's type system to Python. With the introduction of pattern matching\nin Python 3.12 and all the benefits associated with relevant type checking\nthis paradigm becomes more and more usable.\n\nCurrently the main focus of `encrustable` is to port over `Option` and\n`Result` types, the very basic building blocks of Rust's type safety.\n\n## Using `encrustable`\n\n### Installation\n\n```bash\npython -m pip install encrustable\n```\n\n### Basic usage of `Option`\n\nRun the example with `python -m encrustable.examples.option`:\n\n```python\nfrom typing import NewType\n\nfrom encrustable import Nothing, Option, Some\n\n# we use the newtype pattern to squeeze the most ouf of our type checker\nUsername = NewType(\"Username\", str)\nPasswdFileLine = NewType(\"PasswdFileLine\", str)\nParsedPasswdEntry = NewType(\"ParsedPasswdEntry\", list[str])\nShellPath = NewType(\"ShellPath\", str)\n\n\n# we assume the passwd file exists, is readable and consists of valid lines, so we have\n# a function that can fail in exactly one way (the user is not in the passwd file)\n# we encode this scenario as an Option\ndef get_user_entry(username: Username) -> Option[PasswdFileLine]:\n with open(\"/etc/passwd\", \"r\") as passwd_file:\n for line in passwd_file:\n parts: list[str] = line.split(\":\")\n if parts[0] == username:\n return Some(PasswdFileLine(line.strip()))\n return Nothing()\n\n\n# we have a regular function that cannot fail\n# assuming it receives a valid passwd file line\ndef parse_user_entry(entry: PasswdFileLine) -> ParsedPasswdEntry:\n return ParsedPasswdEntry(entry.split(\":\"))\n\n\n# we have another regular function that cannot fail\n# assuming it receives a valid parsed passwd entry\ndef get_shell_name(parsed_entry: ParsedPasswdEntry) -> ShellPath:\n return ShellPath(parsed_entry[6])\n\n\n# now we can combine them and we can skip any error checking midway\n# we will only check the result at the end using pattern matching\ndef print_user_shell(username: Username) -> None:\n match get_user_entry(username) | parse_user_entry | get_shell_name:\n # the result is of the type `Option[ShellPath]`\n case Some(s):\n print(f\"- user '{username}' has shell set to '{s}'\")\n case Nothing():\n print(f\"- user '{username}' not found in the '/etc/passwd' file\")\n\n\n# execute the example for two users, \"root\" and \"anchovies\"\nif __name__ == \"__main__\":\n print_user_shell(Username(\"root\"))\n print_user_shell(Username(\"anchovies\"))\n```\n\n```\n- user 'root' has shell set to '/bin/bash'\n- user 'anchovies' not found in the '/etc/passwd' file\n```\n\n### Basic usage of `Result`\n\nRun the example with `python -m encrustable.examples.result`:\n\n```python\nimport tempfile\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\n\nfrom encrustable import Err, Ok, Result\n\n\n@dataclass(frozen=True, eq=True)\nclass FileReadError(Exception):\n file_name: Path\n original_error: Exception | None = field(default=None, kw_only=True, repr=False)\n\n\nclass FileNotFound(FileReadError):\n pass\n\n\nclass FileNotReadable(FileReadError):\n pass\n\n\nclass FileEmpty(FileReadError):\n pass\n\n\n# this function can fail in many different ways:\n# - file does not exist\n# - file is not readable (because of permissions)\n# - file is empty (so no first line)\n# - we can even have a catch-all generic exception handler if we want to\n# we encode this scenario as a Result\ndef read_first_line_from_file(file_name: Path) -> Result[str, FileReadError]:\n try:\n with file_name.open(\"r\") as file:\n if (line := file.readline().strip()) == \"\":\n return Err(FileEmpty(file_name))\n return Ok(line)\n except FileNotFoundError as e:\n return Err(FileNotFound(file_name, original_error=e))\n except PermissionError as e:\n return Err(FileNotReadable(file_name, original_error=e))\n except Exception as e:\n return Err(FileReadError(file_name, original_error=e))\n\n\n# we have a regular function that cannot fail\n# given a valid string it will give us up to 20 characters from the beginning\ndef trim_str_above_20_len(line: str) -> str:\n return line.strip()[:20]\n\n\n# now we can combine them and we can skip any error checking midway\n# we will only check the result at the end using pattern matching\ndef print_first_line_from_file(file_name: Path) -> None:\n match read_first_line_from_file(file_name) | trim_str_above_20_len:\n case Ok(line):\n print(f\"- First line (up to 20 chars) from '{file_name}': {line}\")\n case Err(err):\n print(\n f\"- Could not read first line from '{str(file_name)[:20]}'\\n\"\n f\" error: {repr(err)}\\n\"\n f\" original error: {repr(err.original_error)}\"\n )\n\n\n# execute the example for a couple of filenames\nif __name__ == \"__main__\":\n print_first_line_from_file(Path(\"/etc/passwd\"))\n print_first_line_from_file(Path(\"/does-not-exist.yaml\"))\n print_first_line_from_file(Path(\"/root/file.yaml\"))\n with tempfile.NamedTemporaryFile(\"w\") as f:\n print_first_line_from_file(Path(f.name))\n```\n\n```\n- First line (up to 20 chars) from '/etc/passwd': root:x:0:0:root:/roo\n- Could not read first line from '/does-not-exist.yaml'\n error: FileNotFound(file_name=PosixPath('/does-not-exist.yaml'))\n original error: [Errno 2] No such file or directory: '/does-not-exist.yaml'\n- Could not read first line from '/root/file.yaml'\n error: FileNotReadable(file_name=PosixPath('/root/file.yaml'))\n original error: [Errno 13] Permission denied: '/root/file.yaml'\n- Could not read first line from '/tmp/tmp1jbyntjo'\n error: FileEmpty(file_name=PosixPath('/tmp/tmp1jbyntjo'))\n original error: None\n```\n\n### The `Panic` boundary\n\nThe boundary between `Option`s/`Result`s and regular Python code can be force-crossed\nby using the `unwrap*` or `expect*` methods, which immediately extracts the contained\nvalue. This can however raise a `Panic` if used when the object state is not the one\nthat's expected, so pattern matching should be the preferred way of extracting values.\n\n`Panic`s are normal Python `Exception`s and can be caught at the boundary.\n\nRun the example with `python -m encrustable.examples.panic`:\n\n```python\nfrom encrustable import Err, Nothing, Ok, Option, Panic, Result, Some\n\n\ndef get_some(v: int) -> Option[int]:\n return Some(v)\n\n\ndef get_nothing() -> Option[int]:\n return Nothing()\n\n\ndef get_ok(v: int) -> Result[int, Exception]:\n return Ok(v)\n\n\ndef get_err(e: Exception) -> Result[int, Exception]:\n return Err(e)\n\n\ndef try_unwrap[T, E: Exception](v: Option[T] | Result[T, E]) -> None:\n try:\n unwrapped = v.unwrap()\n print(f\"- {repr(v)} unwrap: {repr(unwrapped)}\")\n except Panic as p:\n print(f\"- {repr(v)} unwrap: {str(p)}\\n cause: {repr(p.__cause__)}\")\n\n\ndef try_expect[T, E: Exception](v: Option[T] | Result[T, E]) -> None:\n try:\n unwrapped = v.expect(\"no valid value\")\n print(f\"- {repr(v)} expect: {repr(unwrapped)}\")\n except Panic as p:\n print(f\"- {repr(v)} expect: {str(p)}\\n cause: {repr(p.__cause__)}\")\n\n\nif __name__ == \"__main__\":\n try_unwrap(get_some(1))\n try_expect(get_some(1))\n\n try_unwrap(get_nothing())\n try_expect(get_nothing())\n\n try_unwrap(get_ok(1))\n try_expect(get_ok(1))\n\n try_unwrap(get_err(Exception()))\n try_expect(get_err(Exception()))\n```\n\n```\n- Some(v=1) unwrap: 1\n- Some(v=1) expect: 1\n- Nothing() unwrap: panic!\n cause: None\n- Nothing() expect: no valid value\n cause: None\n- Ok(v=1) unwrap: 1\n- Ok(v=1) expect: 1\n- Err(e=Exception()) unwrap: panic!\n cause: Exception()\n- Err(e=Exception()) expect: no valid value\n cause: Exception()\n```\n\n",
"bugtrack_url": null,
"license": "GPL-3.0-or-later",
"summary": "Basic components of Rust's type system magic in Python",
"version": "1.0.0.post2",
"project_urls": {
"Documentation": "https://arcanery.gitlab.io/python/encrustable/",
"Homepage": "https://gitlab.com/arcanery/python/encrustable",
"Repository": "https://gitlab.com/arcanery/python/encrustable"
},
"split_keywords": [],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "967cb95fedd61c65cd3096845d86c593e9635ab11ac7be3b7d731723f91a4b32",
"md5": "0b2cd68065fe827b078e8df94e6a1a7b",
"sha256": "b81732c79ab3476f25d7e30364bbcfd6edcc7f75931c6541cbe3bfc2fafff071"
},
"downloads": -1,
"filename": "encrustable-1.0.0.post2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "0b2cd68065fe827b078e8df94e6a1a7b",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.12",
"size": 27707,
"upload_time": "2025-08-08T20:16:04",
"upload_time_iso_8601": "2025-08-08T20:16:04.223127Z",
"url": "https://files.pythonhosted.org/packages/96/7c/b95fedd61c65cd3096845d86c593e9635ab11ac7be3b7d731723f91a4b32/encrustable-1.0.0.post2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "9fead90de1032f034ce2a63936b2b550c2bbcd3f3439706d0271e50c850c9c06",
"md5": "86084d7300f14f2df56bac814abf8460",
"sha256": "48e82e8fe47b6f4605bdab6f1e0262fb4c6c90ca32eb6e4840f24cace61608e9"
},
"downloads": -1,
"filename": "encrustable-1.0.0.post2.tar.gz",
"has_sig": false,
"md5_digest": "86084d7300f14f2df56bac814abf8460",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.12",
"size": 21794,
"upload_time": "2025-08-08T20:16:05",
"upload_time_iso_8601": "2025-08-08T20:16:05.086139Z",
"url": "https://files.pythonhosted.org/packages/9f/ea/d90de1032f034ce2a63936b2b550c2bbcd3f3439706d0271e50c850c9c06/encrustable-1.0.0.post2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-08-08 20:16:05",
"github": false,
"gitlab": true,
"bitbucket": false,
"codeberg": false,
"gitlab_user": "arcanery",
"gitlab_project": "python",
"lcname": "encrustable"
}